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/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 = { itc: false, init: false, git: false },
51
+ jobList = Object.keys(UnderpostCron.JOB).join(','),
52
+ options = { initPm2Cronjobs: false, git: false, updatePackageScripts: false },
53
53
  ) {
54
- if (options.init === true) {
55
- const jobDeployId = fs.readFileSync('./engine-private/deploy/dd.cron', 'utf8').trim();
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]) await UnderpostCron.JOB[jobId].callback(deployList, options);
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
- * Get the related deploy id for the given job id
130
+ * Update package.json start scripts for specified deploy-ids
77
131
  * @static
78
- * @param {String} jobId - The job id
79
- * @return {String} The related deploy id
132
+ * @param {String} deployList - Comma separated deploy ids
80
133
  * @memberof UnderpostCron
81
134
  */
82
- getRelatedDeployId(jobId) {
83
- switch (jobId) {
84
- case 'dns':
85
- return fs.readFileSync('./engine-private/deploy/dd.cron', 'utf8').trim();
86
- case 'backup':
87
- return fs.readFileSync('./engine-private/deploy/dd.router', 'utf8').trim();
88
- default:
89
- return fs.readFileSync('./engine-private/deploy/dd.cron', 'utf8').trim();
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, nodeNames, namespace = 'default', labelSelector, deployId } = criteria;
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 (options.git === true) {
868
- UnderpostDB.API._manageGitRepo({ repoName, operation: 'clone' });
869
- UnderpostDB.API._manageGitRepo({ repoName, operation: 'pull' });
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
- 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`);
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 && !options.nodeName) {
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.getRelatedDeployId(jobId),
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((options) => {
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('--itc', 'Executes cron jobs within the container execution context.')
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.')