underpost 2.95.1 → 2.95.8
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/README.md +2 -2
- package/bin/deploy.js +6 -26
- package/cli.md +14 -13
- package/conf.js +4 -1
- package/examples/{QUICK-REFERENCE.md → static-page/QUICK-REFERENCE.md} +0 -18
- package/examples/{README.md → static-page/README.md} +3 -44
- package/examples/{STATIC-GENERATOR-GUIDE.md → static-page/STATIC-GENERATOR-GUIDE.md} +0 -50
- package/examples/{ssr-components → static-page/ssr-components}/CustomPage.js +0 -13
- package/examples/{static-config-simple.json → static-page/static-config-example.json} +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/package.json +1 -1
- package/src/cli/baremetal.js +19 -10
- package/src/cli/cloud-init.js +10 -4
- package/src/cli/cron.js +161 -29
- package/src/cli/db.js +67 -100
- package/src/cli/index.js +4 -58
- package/src/cli/repository.js +14 -0
- package/src/cli/run.js +3 -4
- package/src/cli/static.js +99 -194
- package/src/client/services/default/default.management.js +7 -0
- package/src/index.js +1 -1
- package/src/server/backup.js +4 -53
- package/src/server/conf.js +3 -4
- package/examples/static-config-example.json +0 -183
package/src/cli/cron.js
CHANGED
|
@@ -4,12 +4,10 @@
|
|
|
4
4
|
* @namespace UnderpostCron
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { DataBaseProvider } from '../db/DataBaseProvider.js';
|
|
8
7
|
import BackUp from '../server/backup.js';
|
|
9
8
|
import { Cmd } from '../server/conf.js';
|
|
10
9
|
import Dns from '../server/dns.js';
|
|
11
10
|
import { loggerFactory } from '../server/logger.js';
|
|
12
|
-
|
|
13
11
|
import { shellExec } from '../server/process.js';
|
|
14
12
|
import fs from 'fs-extra';
|
|
15
13
|
|
|
@@ -37,57 +35,191 @@ class UnderpostCron {
|
|
|
37
35
|
*/
|
|
38
36
|
backup: BackUp,
|
|
39
37
|
};
|
|
38
|
+
|
|
40
39
|
static API = {
|
|
41
40
|
/**
|
|
42
41
|
* Run the cron jobs
|
|
43
42
|
* @static
|
|
44
43
|
* @param {String} deployList - Comma separated deploy ids
|
|
45
44
|
* @param {String} jobList - Comma separated job ids
|
|
45
|
+
* @param {Object} options - Options for cron execution
|
|
46
46
|
* @return {void}
|
|
47
47
|
* @memberof UnderpostCron
|
|
48
48
|
*/
|
|
49
49
|
callback: async function (
|
|
50
50
|
deployList = 'default',
|
|
51
|
-
jobList = Object.keys(UnderpostCron.JOB),
|
|
52
|
-
options = {
|
|
51
|
+
jobList = Object.keys(UnderpostCron.JOB).join(','),
|
|
52
|
+
options = { initPm2Cronjobs: false, git: false, updatePackageScripts: false },
|
|
53
53
|
) {
|
|
54
|
-
if (options.
|
|
55
|
-
|
|
56
|
-
deployList = fs.readFileSync('./engine-private/deploy/dd.router', 'utf8').trim();
|
|
57
|
-
const confCronConfig = JSON.parse(fs.readFileSync(`./engine-private/conf/${jobDeployId}/conf.cron.json`));
|
|
58
|
-
if (confCronConfig.jobs && Object.keys(confCronConfig.jobs).length > 0) {
|
|
59
|
-
for (const job of Object.keys(confCronConfig.jobs)) {
|
|
60
|
-
const name = `${jobDeployId}-${job}`;
|
|
61
|
-
let deployId;
|
|
62
|
-
shellExec(Cmd.delete(name));
|
|
63
|
-
deployId = UnderpostCron.API.getRelatedDeployId(job);
|
|
64
|
-
shellExec(Cmd.cron(deployId, job, name, confCronConfig.jobs[job].expression, options));
|
|
65
|
-
}
|
|
66
|
-
}
|
|
54
|
+
if (options.updatePackageScripts === true) {
|
|
55
|
+
await UnderpostCron.API.updatePackageScripts(deployList);
|
|
67
56
|
return;
|
|
68
57
|
}
|
|
58
|
+
|
|
59
|
+
if (options.initPm2Cronjobs === true) {
|
|
60
|
+
await UnderpostCron.API.initCronJobs(options);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Execute the requested jobs
|
|
69
65
|
for (const _jobId of jobList.split(',')) {
|
|
70
66
|
const jobId = _jobId.trim();
|
|
71
|
-
if (UnderpostCron.JOB[jobId])
|
|
67
|
+
if (UnderpostCron.JOB[jobId]) {
|
|
68
|
+
logger.info(`Executing cron job: ${jobId}`);
|
|
69
|
+
await UnderpostCron.JOB[jobId].callback(deployList, options);
|
|
70
|
+
} else {
|
|
71
|
+
logger.warn(`Unknown cron job: ${jobId}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Initialize PM2 cron jobs from configuration
|
|
78
|
+
* @static
|
|
79
|
+
* @param {Object} options - Initialization options
|
|
80
|
+
* @memberof UnderpostCron
|
|
81
|
+
*/
|
|
82
|
+
initCronJobs: async function (options = { git: false }) {
|
|
83
|
+
logger.info('Initializing PM2 cron jobs');
|
|
84
|
+
|
|
85
|
+
// Read cron job deployment ID from dd.cron file (e.g., "dd-cron")
|
|
86
|
+
const jobDeployId = fs.readFileSync('./engine-private/deploy/dd.cron', 'utf8').trim();
|
|
87
|
+
const confCronPath = `./engine-private/conf/${jobDeployId}/conf.cron.json`;
|
|
88
|
+
|
|
89
|
+
if (!fs.existsSync(confCronPath)) {
|
|
90
|
+
logger.warn(`Cron configuration not found: ${confCronPath}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const confCronConfig = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
|
|
95
|
+
|
|
96
|
+
if (!confCronConfig.jobs || Object.keys(confCronConfig.jobs).length === 0) {
|
|
97
|
+
logger.info('No cron jobs configured');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Delete all existing cron jobs
|
|
102
|
+
for (const job of Object.keys(confCronConfig.jobs)) {
|
|
103
|
+
const name = `${jobDeployId}-${job}`;
|
|
104
|
+
logger.info(`Removing existing PM2 process: ${name}`);
|
|
105
|
+
shellExec(Cmd.delete(name));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Create PM2 cron jobs for each configured job
|
|
109
|
+
for (const job of Object.keys(confCronConfig.jobs)) {
|
|
110
|
+
const jobConfig = confCronConfig.jobs[job];
|
|
111
|
+
|
|
112
|
+
if (jobConfig.enabled === false) {
|
|
113
|
+
logger.info(`Skipping disabled job: ${job}`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const name = `${jobDeployId}-${job}`;
|
|
118
|
+
const deployIdList = UnderpostCron.API.getRelatedDeployIdList(job);
|
|
119
|
+
const expression = jobConfig.expression || '0 0 * * *'; // Default: daily at midnight
|
|
120
|
+
const instances = jobConfig.instances || 1; // Default: 1 instance
|
|
121
|
+
|
|
122
|
+
logger.info(`Creating PM2 cron job: ${name} with expression: ${expression}, instances: ${instances}`);
|
|
123
|
+
shellExec(Cmd.cron(deployIdList, job, name, expression, options, instances));
|
|
72
124
|
}
|
|
125
|
+
|
|
126
|
+
logger.info('PM2 cron jobs initialization completed');
|
|
73
127
|
},
|
|
74
128
|
|
|
75
129
|
/**
|
|
76
|
-
*
|
|
130
|
+
* Update package.json start scripts for specified deploy-ids
|
|
77
131
|
* @static
|
|
78
|
-
* @param {String}
|
|
79
|
-
* @return {String} The related deploy id
|
|
132
|
+
* @param {String} deployList - Comma separated deploy ids
|
|
80
133
|
* @memberof UnderpostCron
|
|
81
134
|
*/
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
135
|
+
updatePackageScripts: async function (deployList = 'default') {
|
|
136
|
+
logger.info('Updating package.json start scripts for deploy-id configurations');
|
|
137
|
+
|
|
138
|
+
// Resolve deploy list
|
|
139
|
+
if ((!deployList || deployList === 'dd') && fs.existsSync(`./engine-private/deploy/dd.router`)) {
|
|
140
|
+
deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').trim();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const confDir = './engine-private/conf';
|
|
144
|
+
if (!fs.existsSync(confDir)) {
|
|
145
|
+
logger.warn(`Configuration directory not found: ${confDir}`);
|
|
146
|
+
return;
|
|
90
147
|
}
|
|
148
|
+
|
|
149
|
+
// Parse deploy list into array
|
|
150
|
+
const deployIds = deployList
|
|
151
|
+
.split(',')
|
|
152
|
+
.map((id) => id.trim())
|
|
153
|
+
.filter((id) => id);
|
|
154
|
+
|
|
155
|
+
for (const deployId of deployIds) {
|
|
156
|
+
const packageJsonPath = `${confDir}/${deployId}/package.json`;
|
|
157
|
+
const confCronPath = `${confDir}/${deployId}/conf.cron.json`;
|
|
158
|
+
|
|
159
|
+
// Only update if both package.json and conf.cron.json exist
|
|
160
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
161
|
+
logger.info(`Skipping ${deployId}: package.json not found`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!fs.existsSync(confCronPath)) {
|
|
166
|
+
logger.info(`Skipping ${deployId}: conf.cron.json not found`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
171
|
+
const confCron = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
|
|
172
|
+
|
|
173
|
+
// Build start script based on cron jobs configuration
|
|
174
|
+
if (confCron.jobs && Object.keys(confCron.jobs).length > 0) {
|
|
175
|
+
const hasEnabledJobs = Object.values(confCron.jobs).some((job) => job.enabled !== false);
|
|
176
|
+
|
|
177
|
+
if (hasEnabledJobs) {
|
|
178
|
+
// Update start script with PM2 cron jobs initialization
|
|
179
|
+
const startScript = 'pm2 flush && pm2 reloadLogs && node bin cron --init-pm2-cronjobs --git';
|
|
180
|
+
|
|
181
|
+
if (!packageJson.scripts) {
|
|
182
|
+
packageJson.scripts = {};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
packageJson.scripts.start = startScript;
|
|
186
|
+
|
|
187
|
+
// Write updated package.json
|
|
188
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4) + '\n', 'utf8');
|
|
189
|
+
logger.info(`Updated package.json for ${deployId} with cron start script`);
|
|
190
|
+
} else {
|
|
191
|
+
logger.info(`Skipping ${deployId}: no enabled cron jobs`);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
logger.info(`Skipping ${deployId}: no cron jobs configured`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
logger.info('Package.json start scripts update completed');
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get the related deploy id list for the given job id
|
|
203
|
+
* @static
|
|
204
|
+
* @param {String} jobId - The job id (e.g., 'dns', 'backup')
|
|
205
|
+
* @return {String} Comma-separated list of deploy ids to process
|
|
206
|
+
* @memberof UnderpostCron
|
|
207
|
+
*/
|
|
208
|
+
getRelatedDeployIdList(jobId) {
|
|
209
|
+
// Backup job uses dd.router file (contains multiple deploy-ids)
|
|
210
|
+
// Other jobs use dd.cron file (contains single deploy-id)
|
|
211
|
+
const deployFilePath =
|
|
212
|
+
jobId === 'backup' ? './engine-private/deploy/dd.router' : './engine-private/deploy/dd.cron';
|
|
213
|
+
|
|
214
|
+
if (!fs.existsSync(deployFilePath)) {
|
|
215
|
+
logger.warn(`Deploy file not found: ${deployFilePath}, using default`);
|
|
216
|
+
return fs.existsSync('./engine-private/deploy/dd.cron')
|
|
217
|
+
? fs.readFileSync('./engine-private/deploy/dd.cron', 'utf8').trim()
|
|
218
|
+
: 'dd-cron';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Return the deploy-id list from the file (may be single or comma-separated)
|
|
222
|
+
return fs.readFileSync(deployFilePath, 'utf8').trim();
|
|
91
223
|
},
|
|
92
224
|
};
|
|
93
225
|
}
|
package/src/cli/db.js
CHANGED
|
@@ -25,20 +25,12 @@ const logger = loggerFactory(import.meta);
|
|
|
25
25
|
*/
|
|
26
26
|
const MAX_BACKUP_RETENTION = 5;
|
|
27
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
28
|
/**
|
|
36
29
|
* @typedef {Object} DatabaseOptions
|
|
37
30
|
* @memberof UnderpostDB
|
|
38
31
|
* @property {boolean} [import=false] - Flag to import data from a backup
|
|
39
32
|
* @property {boolean} [export=false] - Flag to export data to a backup
|
|
40
33
|
* @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
34
|
* @property {string} [ns='default'] - Kubernetes namespace
|
|
43
35
|
* @property {string} [collections=''] - Comma-separated list of collections to include
|
|
44
36
|
* @property {string} [outPath=''] - Output path for backup files
|
|
@@ -47,10 +39,10 @@ const KUBECTL_TIMEOUT = 300000; // 5 minutes
|
|
|
47
39
|
* @property {boolean} [git=false] - Flag to enable Git integration
|
|
48
40
|
* @property {string} [hosts=''] - Comma-separated list of hosts to include
|
|
49
41
|
* @property {string} [paths=''] - Comma-separated list of paths to include
|
|
50
|
-
* @property {string} [labelSelector=''] - Kubernetes label selector for pods
|
|
51
42
|
* @property {boolean} [allPods=false] - Flag to target all matching pods
|
|
52
43
|
* @property {boolean} [primaryPod=false] - Flag to automatically detect and use MongoDB primary pod
|
|
53
44
|
* @property {boolean} [stats=false] - Flag to display collection/table statistics
|
|
45
|
+
* @property {boolean} [forceClone=false] - Flag to force remove and re-clone cron backup repository
|
|
54
46
|
*/
|
|
55
47
|
|
|
56
48
|
/**
|
|
@@ -84,43 +76,17 @@ const KUBECTL_TIMEOUT = 300000; // 5 minutes
|
|
|
84
76
|
*/
|
|
85
77
|
class UnderpostDB {
|
|
86
78
|
static API = {
|
|
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
79
|
/**
|
|
112
80
|
* Helper: Gets filtered pods based on criteria
|
|
113
81
|
* @private
|
|
114
82
|
* @param {Object} criteria - Filter criteria
|
|
115
83
|
* @param {string} [criteria.podNames] - Comma-separated pod name patterns
|
|
116
|
-
* @param {string} [criteria.nodeNames] - Comma-separated node names
|
|
117
84
|
* @param {string} [criteria.namespace='default'] - Kubernetes namespace
|
|
118
|
-
* @param {string} [criteria.labelSelector] - Label selector
|
|
119
85
|
* @param {string} [criteria.deployId] - Deployment ID pattern
|
|
120
86
|
* @returns {Array<PodInfo>} Filtered pod list
|
|
121
87
|
*/
|
|
122
88
|
_getFilteredPods(criteria = {}) {
|
|
123
|
-
const { podNames,
|
|
89
|
+
const { podNames, namespace = 'default', deployId } = criteria;
|
|
124
90
|
|
|
125
91
|
try {
|
|
126
92
|
// Get all pods using UnderpostDeploy.API.get
|
|
@@ -138,25 +104,6 @@ class UnderpostDB {
|
|
|
138
104
|
});
|
|
139
105
|
}
|
|
140
106
|
|
|
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
107
|
logger.info(`Found ${pods.length} pod(s) matching criteria`, { criteria, podNames: pods.map((p) => p.NAME) });
|
|
161
108
|
return pods;
|
|
162
109
|
} catch (error) {
|
|
@@ -253,9 +200,10 @@ class UnderpostDB {
|
|
|
253
200
|
* @param {string} params.repoName - Repository name
|
|
254
201
|
* @param {string} params.operation - Operation (clone, pull, commit, push)
|
|
255
202
|
* @param {string} [params.message=''] - Commit message
|
|
203
|
+
* @param {boolean} [params.forceClone=false] - Force remove and re-clone repository
|
|
256
204
|
* @returns {boolean} Success status
|
|
257
205
|
*/
|
|
258
|
-
_manageGitRepo({ repoName, operation, message = '' }) {
|
|
206
|
+
_manageGitRepo({ repoName, operation, message = '', forceClone = false }) {
|
|
259
207
|
try {
|
|
260
208
|
const username = process.env.GITHUB_USERNAME;
|
|
261
209
|
if (!username) {
|
|
@@ -267,6 +215,10 @@ class UnderpostDB {
|
|
|
267
215
|
|
|
268
216
|
switch (operation) {
|
|
269
217
|
case 'clone':
|
|
218
|
+
if (forceClone && fs.existsSync(repoPath)) {
|
|
219
|
+
logger.info(`Force clone enabled, removing existing repository: ${repoName}`);
|
|
220
|
+
fs.removeSync(repoPath);
|
|
221
|
+
}
|
|
270
222
|
if (!fs.existsSync(repoPath)) {
|
|
271
223
|
shellExec(`cd .. && underpost clone ${username}/${repoName}`);
|
|
272
224
|
logger.info(`Cloned repository: ${repoName}`);
|
|
@@ -718,9 +670,6 @@ class UnderpostDB {
|
|
|
718
670
|
* @param {string} [options.podName='mongodb-0'] - Initial pod name to query replica set status
|
|
719
671
|
* @returns {string|null} Primary pod name or null if not found
|
|
720
672
|
* @memberof UnderpostDB
|
|
721
|
-
* @example
|
|
722
|
-
* const primaryPod = UnderpostDB.API.getMongoPrimaryPodName({ namespace: 'production' });
|
|
723
|
-
* console.log(primaryPod); // 'mongodb-1'
|
|
724
673
|
*/
|
|
725
674
|
getMongoPrimaryPodName(options = { namespace: 'default', podName: 'mongodb-0' }) {
|
|
726
675
|
const { namespace = 'default', podName = 'mongodb-0' } = options;
|
|
@@ -766,7 +715,6 @@ class UnderpostDB {
|
|
|
766
715
|
* @param {boolean} [options.import=false] - Whether to perform import operation
|
|
767
716
|
* @param {boolean} [options.export=false] - Whether to perform export operation
|
|
768
717
|
* @param {string} [options.podName=''] - Comma-separated pod name patterns to target
|
|
769
|
-
* @param {string} [options.nodeName=''] - Comma-separated node names to target
|
|
770
718
|
* @param {string} [options.ns='default'] - Kubernetes namespace
|
|
771
719
|
* @param {string} [options.collections=''] - Comma-separated MongoDB collections for export
|
|
772
720
|
* @param {string} [options.outPath=''] - Output path for backups
|
|
@@ -775,7 +723,6 @@ class UnderpostDB {
|
|
|
775
723
|
* @param {boolean} [options.git=false] - Whether to use Git for backup versioning
|
|
776
724
|
* @param {string} [options.hosts=''] - Comma-separated list of hosts to filter databases
|
|
777
725
|
* @param {string} [options.paths=''] - Comma-separated list of paths to filter databases
|
|
778
|
-
* @param {string} [options.labelSelector=''] - Label selector for pod filtering
|
|
779
726
|
* @param {boolean} [options.allPods=false] - Whether to target all pods in deployment
|
|
780
727
|
* @param {boolean} [options.primaryPod=false] - Whether to target MongoDB primary pod only
|
|
781
728
|
* @param {boolean} [options.stats=false] - Whether to display database statistics
|
|
@@ -789,7 +736,6 @@ class UnderpostDB {
|
|
|
789
736
|
import: false,
|
|
790
737
|
export: false,
|
|
791
738
|
podName: '',
|
|
792
|
-
nodeName: '',
|
|
793
739
|
ns: 'default',
|
|
794
740
|
collections: '',
|
|
795
741
|
outPath: '',
|
|
@@ -798,22 +744,16 @@ class UnderpostDB {
|
|
|
798
744
|
git: false,
|
|
799
745
|
hosts: '',
|
|
800
746
|
paths: '',
|
|
801
|
-
labelSelector: '',
|
|
802
747
|
allPods: false,
|
|
803
748
|
primaryPod: false,
|
|
804
749
|
stats: false,
|
|
805
750
|
macroRollbackExport: 1,
|
|
751
|
+
forceClone: false,
|
|
806
752
|
},
|
|
807
753
|
) {
|
|
808
754
|
const newBackupTimestamp = new Date().getTime();
|
|
809
755
|
const namespace = options.ns && typeof options.ns === 'string' ? options.ns : 'default';
|
|
810
756
|
|
|
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
757
|
if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
|
|
818
758
|
|
|
819
759
|
logger.info('Starting database operation', {
|
|
@@ -823,6 +763,11 @@ class UnderpostDB {
|
|
|
823
763
|
export: options.export,
|
|
824
764
|
});
|
|
825
765
|
|
|
766
|
+
// Track processed repositories to avoid duplicate Git operations
|
|
767
|
+
const processedRepos = new Set();
|
|
768
|
+
// Track processed host+path combinations to avoid duplicates
|
|
769
|
+
const processedHostPaths = new Set();
|
|
770
|
+
|
|
826
771
|
for (const _deployId of deployList.split(',')) {
|
|
827
772
|
const deployId = _deployId.trim();
|
|
828
773
|
if (!deployId) continue;
|
|
@@ -831,7 +776,7 @@ class UnderpostDB {
|
|
|
831
776
|
|
|
832
777
|
/** @type {Object.<string, Object.<string, DatabaseConfig>>} */
|
|
833
778
|
const dbs = {};
|
|
834
|
-
const repoName = `engine-${deployId.split('dd-')[1]}-cron-backups`;
|
|
779
|
+
const repoName = `engine-${deployId.includes('dd-') ? deployId.split('dd-')[1] : deployId}-cron-backups`;
|
|
835
780
|
|
|
836
781
|
// Load server configuration
|
|
837
782
|
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
@@ -863,31 +808,42 @@ class UnderpostDB {
|
|
|
863
808
|
}
|
|
864
809
|
}
|
|
865
810
|
|
|
866
|
-
// Handle Git operations
|
|
867
|
-
if (
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
811
|
+
// Handle Git operations - execute only once per repository
|
|
812
|
+
if (!processedRepos.has(repoName)) {
|
|
813
|
+
logger.info('Processing Git operations for repository', { repoName, deployId });
|
|
814
|
+
if (options.git === true) {
|
|
815
|
+
UnderpostDB.API._manageGitRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
|
|
816
|
+
UnderpostDB.API._manageGitRepo({ repoName, operation: 'pull' });
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (options.macroRollbackExport) {
|
|
820
|
+
// Only clone if not already done by git option above
|
|
821
|
+
if (options.git !== true) {
|
|
822
|
+
UnderpostDB.API._manageGitRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
|
|
823
|
+
UnderpostDB.API._manageGitRepo({ repoName, operation: 'pull' });
|
|
824
|
+
}
|
|
871
825
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
} else {
|
|
888
|
-
if (!username) logger.error('GITHUB_USERNAME environment variable not set');
|
|
889
|
-
logger.warn('Repository not found for macro rollback', { repoPath });
|
|
826
|
+
const nCommits = parseInt(options.macroRollbackExport);
|
|
827
|
+
const repoPath = `../${repoName}`;
|
|
828
|
+
const username = process.env.GITHUB_USERNAME;
|
|
829
|
+
|
|
830
|
+
if (fs.existsSync(repoPath) && username) {
|
|
831
|
+
logger.info('Executing macro rollback export', { repoName, nCommits });
|
|
832
|
+
shellExec(`cd ${repoPath} && underpost cmt . reset ${nCommits}`);
|
|
833
|
+
shellExec(`cd ${repoPath} && git reset`);
|
|
834
|
+
shellExec(`cd ${repoPath} && git checkout .`);
|
|
835
|
+
shellExec(`cd ${repoPath} && git clean -f -d`);
|
|
836
|
+
shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName} -f`);
|
|
837
|
+
} else {
|
|
838
|
+
if (!username) logger.error('GITHUB_USERNAME environment variable not set');
|
|
839
|
+
logger.warn('Repository not found for macro rollback', { repoPath });
|
|
840
|
+
}
|
|
890
841
|
}
|
|
842
|
+
|
|
843
|
+
processedRepos.add(repoName);
|
|
844
|
+
logger.info('Repository marked as processed', { repoName });
|
|
845
|
+
} else {
|
|
846
|
+
logger.info('Skipping Git operations for already processed repository', { repoName, deployId });
|
|
891
847
|
}
|
|
892
848
|
|
|
893
849
|
// Process each database provider
|
|
@@ -895,6 +851,15 @@ class UnderpostDB {
|
|
|
895
851
|
for (const dbName of Object.keys(dbs[provider])) {
|
|
896
852
|
const { hostFolder, user, password, host, path } = dbs[provider][dbName];
|
|
897
853
|
|
|
854
|
+
// Create unique identifier for host+path combination
|
|
855
|
+
const hostPathKey = `${deployId}:${host}:${path}`;
|
|
856
|
+
|
|
857
|
+
// Skip if this host+path combination was already processed
|
|
858
|
+
if (processedHostPaths.has(hostPathKey)) {
|
|
859
|
+
logger.info('Skipping already processed host/path', { dbName, host, path, deployId });
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
|
|
898
863
|
// Filter by hosts and paths if specified
|
|
899
864
|
if (
|
|
900
865
|
(options.hosts &&
|
|
@@ -917,7 +882,7 @@ class UnderpostDB {
|
|
|
917
882
|
continue;
|
|
918
883
|
}
|
|
919
884
|
|
|
920
|
-
logger.info('Processing database', { hostFolder, provider, dbName });
|
|
885
|
+
logger.info('Processing database', { hostFolder, provider, dbName, deployId });
|
|
921
886
|
|
|
922
887
|
const backUpPath = `../${repoName}/${hostFolder}`;
|
|
923
888
|
const backupInfo = UnderpostDB.API._manageBackupTimestamps(
|
|
@@ -949,16 +914,14 @@ class UnderpostDB {
|
|
|
949
914
|
let targetPods = [];
|
|
950
915
|
const podCriteria = {
|
|
951
916
|
podNames: options.podName,
|
|
952
|
-
nodeNames: options.nodeName,
|
|
953
917
|
namespace,
|
|
954
|
-
labelSelector: options.labelSelector,
|
|
955
918
|
deployId: provider === 'mariadb' ? 'mariadb' : 'mongo',
|
|
956
919
|
};
|
|
957
920
|
|
|
958
921
|
targetPods = UnderpostDB.API._getFilteredPods(podCriteria);
|
|
959
922
|
|
|
960
923
|
// Fallback to default if no custom pods specified
|
|
961
|
-
if (targetPods.length === 0 && !options.podName
|
|
924
|
+
if (targetPods.length === 0 && !options.podName) {
|
|
962
925
|
const defaultPods = UnderpostDeploy.API.get(
|
|
963
926
|
provider === 'mariadb' ? 'mariadb' : 'mongo',
|
|
964
927
|
'pods',
|
|
@@ -1094,16 +1057,20 @@ class UnderpostDB {
|
|
|
1094
1057
|
break;
|
|
1095
1058
|
}
|
|
1096
1059
|
}
|
|
1060
|
+
|
|
1061
|
+
// Mark this host+path combination as processed
|
|
1062
|
+
processedHostPaths.add(hostPathKey);
|
|
1097
1063
|
}
|
|
1098
1064
|
}
|
|
1099
1065
|
|
|
1100
|
-
// Commit and push to Git if enabled
|
|
1101
|
-
if (options.export === true && options.git === true) {
|
|
1066
|
+
// Commit and push to Git if enabled - execute only once per repository
|
|
1067
|
+
if (options.export === true && options.git === true && !processedRepos.has(`${repoName}-committed`)) {
|
|
1102
1068
|
const commitMessage = `${new Date(newBackupTimestamp).toLocaleDateString()} ${new Date(
|
|
1103
1069
|
newBackupTimestamp,
|
|
1104
1070
|
).toLocaleTimeString()}`;
|
|
1105
1071
|
UnderpostDB.API._manageGitRepo({ repoName, operation: 'commit', message: commitMessage });
|
|
1106
1072
|
UnderpostDB.API._manageGitRepo({ repoName, operation: 'push' });
|
|
1073
|
+
processedRepos.add(`${repoName}-committed`);
|
|
1107
1074
|
}
|
|
1108
1075
|
}
|
|
1109
1076
|
|
|
@@ -1267,7 +1234,7 @@ class UnderpostDB {
|
|
|
1267
1234
|
for (const jobId of Object.keys(confCron.jobs)) {
|
|
1268
1235
|
const body = {
|
|
1269
1236
|
jobId,
|
|
1270
|
-
deployId: UnderpostCron.API.
|
|
1237
|
+
deployId: UnderpostCron.API.getRelatedDeployIdList(jobId),
|
|
1271
1238
|
expression: confCron.jobs[jobId].expression,
|
|
1272
1239
|
enabled: confCron.jobs[jobId].enabled,
|
|
1273
1240
|
};
|
package/src/cli/index.js
CHANGED
|
@@ -172,59 +172,7 @@ program
|
|
|
172
172
|
.option('--dev', 'Sets the development cli context')
|
|
173
173
|
|
|
174
174
|
.description(`Manages static build of page, bundles, and documentation with comprehensive customization options.`)
|
|
175
|
-
.action(
|
|
176
|
-
// Handle config template generation
|
|
177
|
-
if (options.generateConfig) {
|
|
178
|
-
const configPath = typeof options.generateConfig === 'string' ? options.generateConfig : './static-config.json';
|
|
179
|
-
return UnderpostStatic.API.generateConfigTemplate(configPath);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Parse comma-separated options
|
|
183
|
-
if (options.keywords) {
|
|
184
|
-
options.keywords = options.keywords.split(',').map((k) => k.trim());
|
|
185
|
-
}
|
|
186
|
-
if (options.headScripts) {
|
|
187
|
-
options.scripts = options.scripts || {};
|
|
188
|
-
options.scripts.head = options.headScripts.split(',').map((s) => ({ src: s.trim() }));
|
|
189
|
-
}
|
|
190
|
-
if (options.bodyScripts) {
|
|
191
|
-
options.scripts = options.scripts || {};
|
|
192
|
-
options.scripts.body = options.bodyScripts.split(',').map((s) => ({ src: s.trim() }));
|
|
193
|
-
}
|
|
194
|
-
if (options.styles) {
|
|
195
|
-
options.styles = options.styles.split(',').map((s) => ({ href: s.trim() }));
|
|
196
|
-
}
|
|
197
|
-
if (options.headComponents) {
|
|
198
|
-
options.headComponents = options.headComponents.split(',').map((c) => c.trim());
|
|
199
|
-
}
|
|
200
|
-
if (options.bodyComponents) {
|
|
201
|
-
options.bodyComponents = options.bodyComponents.split(',').map((c) => c.trim());
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Build metadata object from individual options
|
|
205
|
-
options.metadata = {
|
|
206
|
-
...(options.title && { title: options.title }),
|
|
207
|
-
...(options.description && { description: options.description }),
|
|
208
|
-
...(options.keywords && { keywords: options.keywords }),
|
|
209
|
-
...(options.author && { author: options.author }),
|
|
210
|
-
...(options.themeColor && { themeColor: options.themeColor }),
|
|
211
|
-
...(options.canonicalUrl && { canonicalURL: options.canonicalUrl }),
|
|
212
|
-
...(options.thumbnail && { thumbnail: options.thumbnail }),
|
|
213
|
-
...(options.locale && { locale: options.locale }),
|
|
214
|
-
...(options.siteName && { siteName: options.siteName }),
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
// Build icons object
|
|
218
|
-
if (options.favicon || options.appleTouchIcon || options.manifest) {
|
|
219
|
-
options.icons = {
|
|
220
|
-
...(options.favicon && { favicon: options.favicon }),
|
|
221
|
-
...(options.appleTouchIcon && { appleTouchIcon: options.appleTouchIcon }),
|
|
222
|
-
...(options.manifest && { manifest: options.manifest }),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return UnderpostStatic.API.callback(options);
|
|
227
|
-
});
|
|
175
|
+
.action(UnderpostStatic.API.callback);
|
|
228
176
|
|
|
229
177
|
// 'config' command: Manage Underpost configurations
|
|
230
178
|
program
|
|
@@ -417,8 +365,6 @@ program
|
|
|
417
365
|
'--pod-name <pod-name>',
|
|
418
366
|
'Comma-separated list of pod names or patterns (supports wildcards like "mariadb-*").',
|
|
419
367
|
)
|
|
420
|
-
.option('--node-name <node-name>', 'Comma-separated list of node names to filter pods by their node placement.')
|
|
421
|
-
.option('--label-selector <selector>', 'Kubernetes label selector for filtering pods (e.g., "app=mariadb").')
|
|
422
368
|
.option('--all-pods', 'Target all matching pods instead of just the first one.')
|
|
423
369
|
.option('--primary-pod', 'Automatically detect and use MongoDB primary pod (MongoDB only).')
|
|
424
370
|
.option('--stats', 'Display database statistics (collection/table names with document/row counts).')
|
|
@@ -427,6 +373,7 @@ program
|
|
|
427
373
|
.option('--drop', 'Drops the specified databases or collections before importing.')
|
|
428
374
|
.option('--preserveUUID', 'Preserves UUIDs during database import operations.')
|
|
429
375
|
.option('--git', 'Enables Git integration for backup version control (clone, pull, commit, push to GitHub).')
|
|
376
|
+
.option('--force-clone', 'Forces cloning of the Git repository, overwriting local changes.')
|
|
430
377
|
.option('--hosts <hosts>', 'Comma-separated list of database hosts to filter operations.')
|
|
431
378
|
.option('--paths <paths>', 'Comma-separated list of paths to filter database operations.')
|
|
432
379
|
.option('--ns <ns-name>', 'Kubernetes namespace context for database operations (defaults to "default").')
|
|
@@ -478,9 +425,9 @@ program
|
|
|
478
425
|
', ',
|
|
479
426
|
)}. Defaults to all available jobs.`,
|
|
480
427
|
)
|
|
481
|
-
.option('--
|
|
482
|
-
.option('--init', 'Initializes cron jobs for the default deployment ID.')
|
|
428
|
+
.option('--init-pm2-cronjobs', 'Initializes PM2 cron jobs from configuration for the specified deployment IDs.')
|
|
483
429
|
.option('--git', 'Uploads cron job configurations to GitHub.')
|
|
430
|
+
.option('--update-package-scripts', 'Updates package.json start scripts for each deploy-id configuration.')
|
|
484
431
|
.description('Manages cron jobs, including initialization, execution, and configuration updates.')
|
|
485
432
|
.action(Underpost.cron.callback);
|
|
486
433
|
|
|
@@ -669,7 +616,6 @@ program
|
|
|
669
616
|
.option('--nfs-unmount', 'Unmounts the NFS root filesystem for a workflow id config architecture.')
|
|
670
617
|
.option('--nfs-sh', 'Copies QEMU emulation root entrypoint shell command to the clipboard.')
|
|
671
618
|
.option('--cloud-init-update', 'Updates cloud init for a workflow id config architecture.')
|
|
672
|
-
.option('--cloud-init-reset', 'Resets cloud init for a workflow id config architecture.')
|
|
673
619
|
.option('--logs <log-id>', 'Displays logs for log id: dhcp, cloud, machine, cloud-config.')
|
|
674
620
|
.option('--dev', 'Sets the development context environment for baremetal operations.')
|
|
675
621
|
.option('--ls', 'Lists available boot resources and machines.')
|