underpost 2.95.3 → 2.96.0
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/baremetal/commission-workflows.json +44 -0
- package/baremetal/packer-workflows.json +13 -0
- package/bin/deploy.js +6 -26
- package/cli.md +40 -43
- 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/packer/images/Rocky9Amd64/Makefile +62 -0
- package/packer/images/Rocky9Amd64/QUICKSTART.md +113 -0
- package/packer/images/Rocky9Amd64/README.md +122 -0
- package/packer/images/Rocky9Amd64/http/rocky9.ks.pkrtpl.hcl +114 -0
- package/packer/images/Rocky9Amd64/rocky9.pkr.hcl +160 -0
- package/packer/scripts/fuse-nbd +64 -0
- package/packer/scripts/fuse-tar-root +63 -0
- package/scripts/maas-setup.sh +13 -2
- package/scripts/maas-upload-boot-resource.sh +183 -0
- package/scripts/packer-init-vars-file.sh +30 -0
- package/scripts/packer-setup.sh +52 -0
- package/src/cli/baremetal.js +262 -65
- package/src/cli/cloud-init.js +11 -5
- package/src/cli/cron.js +161 -29
- package/src/cli/db.js +59 -92
- package/src/cli/env.js +24 -3
- package/src/cli/index.js +18 -58
- package/src/cli/repository.js +178 -0
- package/src/cli/run.js +2 -3
- 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/client/ssr/pages/404.js +0 -12
- package/src/client/ssr/pages/500.js +0 -12
- package/src/client/ssr/pages/maintenance.js +0 -14
- package/src/client/ssr/pages/offline.js +0 -21
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
|
@@ -31,7 +31,6 @@ const MAX_BACKUP_RETENTION = 5;
|
|
|
31
31
|
* @property {boolean} [import=false] - Flag to import data from a backup
|
|
32
32
|
* @property {boolean} [export=false] - Flag to export data to a backup
|
|
33
33
|
* @property {string} [podName=''] - Comma-separated list of pod names or patterns
|
|
34
|
-
* @property {string} [nodeName=''] - Comma-separated list of node names for pod filtering
|
|
35
34
|
* @property {string} [ns='default'] - Kubernetes namespace
|
|
36
35
|
* @property {string} [collections=''] - Comma-separated list of collections to include
|
|
37
36
|
* @property {string} [outPath=''] - Output path for backup files
|
|
@@ -40,7 +39,6 @@ const MAX_BACKUP_RETENTION = 5;
|
|
|
40
39
|
* @property {boolean} [git=false] - Flag to enable Git integration
|
|
41
40
|
* @property {string} [hosts=''] - Comma-separated list of hosts to include
|
|
42
41
|
* @property {string} [paths=''] - Comma-separated list of paths to include
|
|
43
|
-
* @property {string} [labelSelector=''] - Kubernetes label selector for pods
|
|
44
42
|
* @property {boolean} [allPods=false] - Flag to target all matching pods
|
|
45
43
|
* @property {boolean} [primaryPod=false] - Flag to automatically detect and use MongoDB primary pod
|
|
46
44
|
* @property {boolean} [stats=false] - Flag to display collection/table statistics
|
|
@@ -78,43 +76,17 @@ const MAX_BACKUP_RETENTION = 5;
|
|
|
78
76
|
*/
|
|
79
77
|
class UnderpostDB {
|
|
80
78
|
static API = {
|
|
81
|
-
/**
|
|
82
|
-
* Helper: Validates namespace name
|
|
83
|
-
* @private
|
|
84
|
-
* @param {string} namespace - Namespace to validate
|
|
85
|
-
* @returns {boolean} True if valid
|
|
86
|
-
*/
|
|
87
|
-
_validateNamespace(namespace) {
|
|
88
|
-
if (!namespace || typeof namespace !== 'string') return false;
|
|
89
|
-
// Kubernetes namespace naming rules: lowercase alphanumeric, -, max 63 chars
|
|
90
|
-
return /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(namespace) && namespace.length <= 63;
|
|
91
|
-
},
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Helper: Validates pod name
|
|
95
|
-
* @private
|
|
96
|
-
* @param {string} podName - Pod name to validate
|
|
97
|
-
* @returns {boolean} True if valid
|
|
98
|
-
*/
|
|
99
|
-
_validatePodName(podName) {
|
|
100
|
-
if (!podName || typeof podName !== 'string') return false;
|
|
101
|
-
// Kubernetes pod naming rules: lowercase alphanumeric, -, max 253 chars
|
|
102
|
-
return /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(podName) && podName.length <= 253;
|
|
103
|
-
},
|
|
104
|
-
|
|
105
79
|
/**
|
|
106
80
|
* Helper: Gets filtered pods based on criteria
|
|
107
81
|
* @private
|
|
108
82
|
* @param {Object} criteria - Filter criteria
|
|
109
83
|
* @param {string} [criteria.podNames] - Comma-separated pod name patterns
|
|
110
|
-
* @param {string} [criteria.nodeNames] - Comma-separated node names
|
|
111
84
|
* @param {string} [criteria.namespace='default'] - Kubernetes namespace
|
|
112
|
-
* @param {string} [criteria.labelSelector] - Label selector
|
|
113
85
|
* @param {string} [criteria.deployId] - Deployment ID pattern
|
|
114
86
|
* @returns {Array<PodInfo>} Filtered pod list
|
|
115
87
|
*/
|
|
116
88
|
_getFilteredPods(criteria = {}) {
|
|
117
|
-
const { podNames,
|
|
89
|
+
const { podNames, namespace = 'default', deployId } = criteria;
|
|
118
90
|
|
|
119
91
|
try {
|
|
120
92
|
// Get all pods using UnderpostDeploy.API.get
|
|
@@ -132,25 +104,6 @@ class UnderpostDB {
|
|
|
132
104
|
});
|
|
133
105
|
}
|
|
134
106
|
|
|
135
|
-
// Filter by node names if specified (only if NODE is not '<none>')
|
|
136
|
-
if (nodeNames) {
|
|
137
|
-
const nodes = nodeNames.split(',').map((n) => n.trim());
|
|
138
|
-
pods = pods.filter((pod) => {
|
|
139
|
-
// Skip filtering if NODE is '<none>' or undefined
|
|
140
|
-
if (!pod.NODE || pod.NODE === '<none>') {
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
return nodes.includes(pod.NODE);
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Filter by label selector if specified
|
|
148
|
-
if (labelSelector) {
|
|
149
|
-
// Note: UnderpostDeploy.API.get doesn't support label selectors directly
|
|
150
|
-
// This would require a separate kubectl command
|
|
151
|
-
logger.warn('Label selector filtering requires additional implementation');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
107
|
logger.info(`Found ${pods.length} pod(s) matching criteria`, { criteria, podNames: pods.map((p) => p.NAME) });
|
|
155
108
|
return pods;
|
|
156
109
|
} catch (error) {
|
|
@@ -717,9 +670,6 @@ class UnderpostDB {
|
|
|
717
670
|
* @param {string} [options.podName='mongodb-0'] - Initial pod name to query replica set status
|
|
718
671
|
* @returns {string|null} Primary pod name or null if not found
|
|
719
672
|
* @memberof UnderpostDB
|
|
720
|
-
* @example
|
|
721
|
-
* const primaryPod = UnderpostDB.API.getMongoPrimaryPodName({ namespace: 'production' });
|
|
722
|
-
* console.log(primaryPod); // 'mongodb-1'
|
|
723
673
|
*/
|
|
724
674
|
getMongoPrimaryPodName(options = { namespace: 'default', podName: 'mongodb-0' }) {
|
|
725
675
|
const { namespace = 'default', podName = 'mongodb-0' } = options;
|
|
@@ -765,7 +715,6 @@ class UnderpostDB {
|
|
|
765
715
|
* @param {boolean} [options.import=false] - Whether to perform import operation
|
|
766
716
|
* @param {boolean} [options.export=false] - Whether to perform export operation
|
|
767
717
|
* @param {string} [options.podName=''] - Comma-separated pod name patterns to target
|
|
768
|
-
* @param {string} [options.nodeName=''] - Comma-separated node names to target
|
|
769
718
|
* @param {string} [options.ns='default'] - Kubernetes namespace
|
|
770
719
|
* @param {string} [options.collections=''] - Comma-separated MongoDB collections for export
|
|
771
720
|
* @param {string} [options.outPath=''] - Output path for backups
|
|
@@ -774,7 +723,6 @@ class UnderpostDB {
|
|
|
774
723
|
* @param {boolean} [options.git=false] - Whether to use Git for backup versioning
|
|
775
724
|
* @param {string} [options.hosts=''] - Comma-separated list of hosts to filter databases
|
|
776
725
|
* @param {string} [options.paths=''] - Comma-separated list of paths to filter databases
|
|
777
|
-
* @param {string} [options.labelSelector=''] - Label selector for pod filtering
|
|
778
726
|
* @param {boolean} [options.allPods=false] - Whether to target all pods in deployment
|
|
779
727
|
* @param {boolean} [options.primaryPod=false] - Whether to target MongoDB primary pod only
|
|
780
728
|
* @param {boolean} [options.stats=false] - Whether to display database statistics
|
|
@@ -788,7 +736,6 @@ class UnderpostDB {
|
|
|
788
736
|
import: false,
|
|
789
737
|
export: false,
|
|
790
738
|
podName: '',
|
|
791
|
-
nodeName: '',
|
|
792
739
|
ns: 'default',
|
|
793
740
|
collections: '',
|
|
794
741
|
outPath: '',
|
|
@@ -797,7 +744,6 @@ class UnderpostDB {
|
|
|
797
744
|
git: false,
|
|
798
745
|
hosts: '',
|
|
799
746
|
paths: '',
|
|
800
|
-
labelSelector: '',
|
|
801
747
|
allPods: false,
|
|
802
748
|
primaryPod: false,
|
|
803
749
|
stats: false,
|
|
@@ -808,12 +754,6 @@ class UnderpostDB {
|
|
|
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/env.js
CHANGED
|
@@ -74,17 +74,38 @@ class UnderpostRootEnv {
|
|
|
74
74
|
/**
|
|
75
75
|
* @method list
|
|
76
76
|
* @description Lists all environment variables in the underpost root environment.
|
|
77
|
+
* @param {string} key - Not used for list operation.
|
|
78
|
+
* @param {string} value - Not used for list operation.
|
|
79
|
+
* @param {object} options - Options for listing environment variables.
|
|
80
|
+
* @param {string} [options.filter] - Filter keyword to match against keys or values.
|
|
77
81
|
* @memberof UnderpostEnv
|
|
78
82
|
*/
|
|
79
|
-
list() {
|
|
83
|
+
list(key, value, options = {}) {
|
|
80
84
|
const exeRootPath = `${getNpmRootPath()}/underpost`;
|
|
81
85
|
const envPath = `${exeRootPath}/.env`;
|
|
82
86
|
if (!fs.existsSync(envPath)) {
|
|
83
87
|
logger.warn(`Empty environment variables`);
|
|
84
88
|
return {};
|
|
85
89
|
}
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
let env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
|
|
91
|
+
|
|
92
|
+
// Apply filter if provided
|
|
93
|
+
if (options.filter) {
|
|
94
|
+
const filterKeyword = options.filter.toLowerCase();
|
|
95
|
+
const filtered = {};
|
|
96
|
+
for (const [envKey, envValue] of Object.entries(env)) {
|
|
97
|
+
const keyMatch = envKey.toLowerCase().includes(filterKeyword);
|
|
98
|
+
const valueMatch = String(envValue).toLowerCase().includes(filterKeyword);
|
|
99
|
+
if (keyMatch || valueMatch) {
|
|
100
|
+
filtered[envKey] = envValue;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
env = filtered;
|
|
104
|
+
logger.info(`underpost root (filtered by: ${options.filter})`, env);
|
|
105
|
+
} else {
|
|
106
|
+
logger.info('underpost root', env);
|
|
107
|
+
}
|
|
108
|
+
|
|
88
109
|
return env;
|
|
89
110
|
},
|
|
90
111
|
/**
|