underpost 3.2.0 → 3.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.ci.yml +7 -0
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -1
- package/.github/workflows/release.cd.yml +10 -5
- package/CHANGELOG.md +83 -1
- package/CLI-HELP.md +9 -5
- package/Dockerfile +4 -2
- package/README.md +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -2
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +5 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +18 -26
- package/package.json +2 -3
- package/src/cli/db.js +671 -622
- package/src/cli/deploy.js +47 -28
- package/src/cli/env.js +28 -0
- package/src/cli/fs.js +3 -1
- package/src/cli/index.js +6 -1
- package/src/cli/repository.js +147 -0
- package/src/cli/run.js +1 -1
- package/src/cli/secrets.js +73 -0
- package/src/client/components/core/DropDown.js +13 -5
- package/src/index.js +1 -1
- package/src/runtime/express/Dockerfile +4 -0
- package/src/runtime/lampp/Dockerfile +4 -0
- package/src/runtime/lampp/Lampp.js +23 -1
- package/src/runtime/wp/Dockerfile +4 -0
- package/src/runtime/wp/Wp.js +148 -6
- package/src/server/backup.js +38 -27
- package/src/server/cron.js +23 -18
- package/src/server/start.js +2 -8
package/src/cli/db.js
CHANGED
|
@@ -549,6 +549,7 @@ class UnderpostDB {
|
|
|
549
549
|
* @param {boolean} [options.k3s=false] - k3s cluster flag.
|
|
550
550
|
* @param {boolean} [options.kubeadm=false] - kubeadm cluster flag.
|
|
551
551
|
* @param {boolean} [options.kind=false] - kind cluster flag.
|
|
552
|
+
* @param {boolean} [options.repoBackup=false] - Backs up repositories (git commit+push) inside deployment pods via kubectl exec.
|
|
552
553
|
* @return {Promise<void>} Resolves when operation is complete.
|
|
553
554
|
*/
|
|
554
555
|
async callback(
|
|
@@ -577,350 +578,375 @@ class UnderpostDB {
|
|
|
577
578
|
k3s: false,
|
|
578
579
|
kubeadm: false,
|
|
579
580
|
kind: false,
|
|
581
|
+
repoBackup: false,
|
|
580
582
|
},
|
|
581
583
|
) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
584
|
+
// Ensure engine-private is available (clone if inside a deployment
|
|
585
|
+
// container where globalSecretClean has already removed it).
|
|
586
|
+
const firstDeployId = deployList !== 'dd' ? deployList.split(',')[0].trim() : '';
|
|
587
|
+
try {
|
|
588
|
+
loadCronDeployEnv();
|
|
589
|
+
const newBackupTimestamp = new Date().getTime();
|
|
590
|
+
const namespace = options.ns && typeof options.ns === 'string' ? options.ns : 'default';
|
|
591
|
+
|
|
592
|
+
if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
|
|
593
|
+
|
|
594
|
+
// Handle repository backup (git commit+push inside deployment pod)
|
|
595
|
+
if (options.repoBackup) {
|
|
596
|
+
const namespace = options.ns && typeof options.ns === 'string' ? options.ns : 'default';
|
|
597
|
+
for (const _deployId of deployList.split(',')) {
|
|
598
|
+
const deployId = _deployId.trim();
|
|
599
|
+
if (!deployId) continue;
|
|
600
|
+
logger.info('Starting pod repository backup', { deployId, namespace });
|
|
601
|
+
Underpost.repo.backupPodRepositories({
|
|
602
|
+
deployId,
|
|
603
|
+
namespace,
|
|
604
|
+
env: options.dev ? 'development' : 'production',
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
598
609
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
610
|
+
// Handle clean-fs-collection operation
|
|
611
|
+
if (options.cleanFsCollection || options.cleanFsDryRun) {
|
|
612
|
+
logger.info('Starting File collection cleanup operation', { deployList });
|
|
613
|
+
await Underpost.db.cleanFsCollection(deployList, {
|
|
614
|
+
hosts: options.hosts,
|
|
615
|
+
paths: options.paths,
|
|
616
|
+
dryRun: options.cleanFsDryRun,
|
|
617
|
+
});
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
605
620
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
621
|
+
logger.info('Starting database operation', {
|
|
622
|
+
deployList,
|
|
623
|
+
namespace,
|
|
624
|
+
import: options.import,
|
|
625
|
+
export: options.export,
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
if (options.primaryPodEnsure) {
|
|
629
|
+
const primaryPodName = Underpost.db.getMongoPrimaryPodName({ namespace, podName: options.primaryPodEnsure });
|
|
630
|
+
if (!primaryPodName) {
|
|
631
|
+
const baseCommand = options.dev ? 'node bin' : 'underpost';
|
|
632
|
+
const baseClusterCommand = options.dev ? ' --dev' : '';
|
|
633
|
+
let clusterFlag = options.k3s ? ' --k3s' : options.kubeadm ? ' --kubeadm' : '';
|
|
634
|
+
shellExec(`${baseCommand} cluster${baseClusterCommand}${clusterFlag} --mongodb`);
|
|
635
|
+
}
|
|
636
|
+
return;
|
|
613
637
|
}
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
638
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
639
|
+
// Track processed repositories to avoid duplicate Git operations
|
|
640
|
+
const processedRepos = new Set();
|
|
641
|
+
// Track processed host+path combinations to avoid duplicates
|
|
642
|
+
const processedHostPaths = new Set();
|
|
621
643
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
644
|
+
for (const _deployId of deployList.split(',')) {
|
|
645
|
+
const deployId = _deployId.trim();
|
|
646
|
+
if (!deployId) continue;
|
|
625
647
|
|
|
626
|
-
|
|
648
|
+
logger.info('Processing deployment', { deployId });
|
|
627
649
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
650
|
+
/** @type {Object.<string, Object.<string, DatabaseConfig>>} */
|
|
651
|
+
const dbs = {};
|
|
652
|
+
const repoName = `engine-${deployId.includes('dd-') ? deployId.split('dd-')[1] : deployId}-cron-backups`;
|
|
631
653
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
654
|
+
// Load server configuration
|
|
655
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
656
|
+
if (!fs.existsSync(confServerPath)) {
|
|
657
|
+
logger.error('Configuration file not found', { path: confServerPath });
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
638
660
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
661
|
+
const confServer = loadConfServerJson(confServerPath, { resolve: true });
|
|
662
|
+
|
|
663
|
+
// Build database configuration map
|
|
664
|
+
for (const host of Object.keys(confServer)) {
|
|
665
|
+
for (const path of Object.keys(confServer[host])) {
|
|
666
|
+
const { db } = confServer[host][path];
|
|
667
|
+
if (db) {
|
|
668
|
+
const { provider, name, user, password } = db;
|
|
669
|
+
if (!dbs[provider]) dbs[provider] = {};
|
|
670
|
+
|
|
671
|
+
if (!(name in dbs[provider])) {
|
|
672
|
+
dbs[provider][name] = {
|
|
673
|
+
user,
|
|
674
|
+
password,
|
|
675
|
+
hostFolder: host + path.replaceAll('/', '-'),
|
|
676
|
+
host,
|
|
677
|
+
path,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
657
680
|
}
|
|
658
681
|
}
|
|
659
682
|
}
|
|
660
|
-
}
|
|
661
683
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
Underpost.repo.manageBackupRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
|
|
667
|
-
Underpost.repo.manageBackupRepo({ repoName, operation: 'pull' });
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
if (options.macroRollbackExport) {
|
|
671
|
-
// Only clone if not already done by git option above
|
|
672
|
-
if (options.git !== true) {
|
|
684
|
+
// Handle Git operations - execute only once per repository
|
|
685
|
+
if (!processedRepos.has(repoName)) {
|
|
686
|
+
logger.info('Processing Git operations for repository', { repoName, deployId });
|
|
687
|
+
if (options.git === true) {
|
|
673
688
|
Underpost.repo.manageBackupRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
|
|
674
689
|
Underpost.repo.manageBackupRepo({ repoName, operation: 'pull' });
|
|
675
690
|
}
|
|
676
691
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
692
|
+
if (options.macroRollbackExport) {
|
|
693
|
+
// Only clone if not already done by git option above
|
|
694
|
+
if (options.git !== true) {
|
|
695
|
+
Underpost.repo.manageBackupRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
|
|
696
|
+
Underpost.repo.manageBackupRepo({ repoName, operation: 'pull' });
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const nCommits = parseInt(options.macroRollbackExport);
|
|
700
|
+
const repoPath = `../${repoName}`;
|
|
701
|
+
const username = process.env.GITHUB_USERNAME;
|
|
702
|
+
|
|
703
|
+
if (fs.existsSync(repoPath) && username) {
|
|
704
|
+
logger.info('Executing macro rollback export', { repoName, nCommits });
|
|
705
|
+
shellExec(`cd ${repoPath} && underpost cmt . reset ${nCommits}`);
|
|
706
|
+
shellExec(`cd ${repoPath} && git reset`);
|
|
707
|
+
shellExec(`cd ${repoPath} && git checkout .`);
|
|
708
|
+
shellExec(`cd ${repoPath} && git clean -f -d`);
|
|
709
|
+
shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName} -f`);
|
|
710
|
+
} else {
|
|
711
|
+
if (!username) logger.error('GITHUB_USERNAME environment variable not set');
|
|
712
|
+
logger.warn('Repository not found for macro rollback', { repoPath });
|
|
713
|
+
}
|
|
691
714
|
}
|
|
692
|
-
}
|
|
693
715
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
716
|
+
processedRepos.add(repoName);
|
|
717
|
+
logger.info('Repository marked as processed', { repoName });
|
|
718
|
+
} else {
|
|
719
|
+
logger.info('Skipping Git operations for already processed repository', { repoName, deployId });
|
|
720
|
+
}
|
|
699
721
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
722
|
+
// Process each database provider
|
|
723
|
+
for (const provider of Object.keys(dbs)) {
|
|
724
|
+
for (const dbName of Object.keys(dbs[provider])) {
|
|
725
|
+
const { hostFolder, user, password, host, path } = dbs[provider][dbName];
|
|
704
726
|
|
|
705
|
-
|
|
706
|
-
|
|
727
|
+
// Create unique identifier for host+path combination
|
|
728
|
+
const hostPathKey = `${deployId}:${host}:${path}`;
|
|
707
729
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
// Filter by hosts and paths if specified
|
|
715
|
-
if (
|
|
716
|
-
(options.hosts &&
|
|
717
|
-
!options.hosts
|
|
718
|
-
.split(',')
|
|
719
|
-
.map((h) => h.trim())
|
|
720
|
-
.includes(host)) ||
|
|
721
|
-
(options.paths &&
|
|
722
|
-
!options.paths
|
|
723
|
-
.split(',')
|
|
724
|
-
.map((p) => p.trim())
|
|
725
|
-
.includes(path))
|
|
726
|
-
) {
|
|
727
|
-
logger.info('Skipping database due to host/path filter', { dbName, host, path });
|
|
728
|
-
continue;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
if (!hostFolder) {
|
|
732
|
-
logger.warn('No hostFolder defined for database', { dbName, provider });
|
|
733
|
-
continue;
|
|
734
|
-
}
|
|
730
|
+
// Skip if this host+path combination was already processed
|
|
731
|
+
if (processedHostPaths.has(hostPathKey)) {
|
|
732
|
+
logger.info('Skipping already processed host/path', { dbName, host, path, deployId });
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
735
|
|
|
736
|
-
|
|
736
|
+
// Filter by hosts and paths if specified
|
|
737
|
+
if (
|
|
738
|
+
(options.hosts &&
|
|
739
|
+
!options.hosts
|
|
740
|
+
.split(',')
|
|
741
|
+
.map((h) => h.trim())
|
|
742
|
+
.includes(host)) ||
|
|
743
|
+
(options.paths &&
|
|
744
|
+
!options.paths
|
|
745
|
+
.split(',')
|
|
746
|
+
.map((p) => p.trim())
|
|
747
|
+
.includes(path))
|
|
748
|
+
) {
|
|
749
|
+
logger.info('Skipping database due to host/path filter', { dbName, host, path });
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
737
752
|
|
|
738
|
-
|
|
753
|
+
if (!hostFolder) {
|
|
754
|
+
logger.warn('No hostFolder defined for database', { dbName, provider });
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
739
757
|
|
|
740
|
-
|
|
758
|
+
logger.info('Processing database', { hostFolder, provider, dbName, deployId });
|
|
741
759
|
|
|
742
|
-
|
|
743
|
-
const sqlContainerPath = `/home/${dbName}.sql`;
|
|
744
|
-
const fromPartsPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}-parths.json`;
|
|
745
|
-
const toSqlPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}.sql`;
|
|
746
|
-
const toNewSqlPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}.sql`;
|
|
747
|
-
const toBsonPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}`;
|
|
748
|
-
const toNewBsonPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}`;
|
|
760
|
+
const latestBackupTimestamp = Underpost.db._getLatestBackupTimestamp(`../${repoName}/${hostFolder}`);
|
|
749
761
|
|
|
750
|
-
|
|
751
|
-
if (options.import === true && fs.existsSync(fromPartsPath) && !fs.existsSync(toSqlPath)) {
|
|
752
|
-
const names = JSON.parse(fs.readFileSync(fromPartsPath, 'utf8')).map((_path) => {
|
|
753
|
-
return `../${repoName}/${hostFolder}/${currentTimestamp}/${_path.split('/').pop()}`;
|
|
754
|
-
});
|
|
755
|
-
logger.info('Merging backup parts', { fromPartsPath, toSqlPath, parts: names.length });
|
|
756
|
-
await mergeFile(names, toSqlPath);
|
|
757
|
-
}
|
|
762
|
+
dbs[provider][dbName].currentBackupTimestamp = latestBackupTimestamp;
|
|
758
763
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
764
|
+
const currentTimestamp = latestBackupTimestamp || newBackupTimestamp;
|
|
765
|
+
const sqlContainerPath = `/home/${dbName}.sql`;
|
|
766
|
+
const fromPartsPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}-parths.json`;
|
|
767
|
+
const toSqlPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}.sql`;
|
|
768
|
+
const toNewSqlPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}.sql`;
|
|
769
|
+
const toBsonPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}`;
|
|
770
|
+
const toNewBsonPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}`;
|
|
766
771
|
|
|
767
|
-
|
|
772
|
+
// Merge split SQL files if needed for import
|
|
773
|
+
if (options.import === true && fs.existsSync(fromPartsPath) && !fs.existsSync(toSqlPath)) {
|
|
774
|
+
const names = JSON.parse(fs.readFileSync(fromPartsPath, 'utf8')).map((_path) => {
|
|
775
|
+
return `../${repoName}/${hostFolder}/${currentTimestamp}/${_path.split('/').pop()}`;
|
|
776
|
+
});
|
|
777
|
+
logger.info('Merging backup parts', { fromPartsPath, toSqlPath, parts: names.length });
|
|
778
|
+
await mergeFile(names, toSqlPath);
|
|
779
|
+
}
|
|
768
780
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
'pods',
|
|
781
|
+
// Get target pods based on provider and options
|
|
782
|
+
let targetPods = [];
|
|
783
|
+
const podCriteria = {
|
|
784
|
+
podNames: options.podName,
|
|
774
785
|
namespace,
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
targetPods = defaultPods;
|
|
778
|
-
}
|
|
786
|
+
deployId: provider === 'mariadb' ? 'mariadb' : 'mongo',
|
|
787
|
+
};
|
|
779
788
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
789
|
+
targetPods = Underpost.kubectl.getFilteredPods(podCriteria);
|
|
790
|
+
|
|
791
|
+
// Fallback to default if no custom pods specified
|
|
792
|
+
if (targetPods.length === 0 && !options.podName) {
|
|
793
|
+
const defaultPods = Underpost.kubectl.get(
|
|
794
|
+
provider === 'mariadb' ? 'mariadb' : 'mongo',
|
|
795
|
+
'pods',
|
|
796
|
+
namespace,
|
|
797
|
+
);
|
|
798
|
+
console.log('defaultPods', defaultPods);
|
|
799
|
+
targetPods = defaultPods;
|
|
800
|
+
}
|
|
784
801
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
802
|
+
if (targetPods.length === 0) {
|
|
803
|
+
logger.warn('No pods found matching criteria', { provider, criteria: podCriteria });
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Handle primary pod detection for MongoDB
|
|
808
|
+
let podsToProcess = [];
|
|
809
|
+
if (provider === 'mongoose' && !options.allPods) {
|
|
810
|
+
// For MongoDB, always use primary pod unless allPods is true
|
|
811
|
+
if (!targetPods || targetPods.length === 0) {
|
|
812
|
+
logger.warn('No MongoDB pods available to check for primary');
|
|
813
|
+
podsToProcess = [];
|
|
814
|
+
} else {
|
|
815
|
+
const firstPod = targetPods[0].NAME;
|
|
816
|
+
const primaryPodName = Underpost.db.getMongoPrimaryPodName({ namespace, podName: firstPod });
|
|
817
|
+
|
|
818
|
+
if (primaryPodName) {
|
|
819
|
+
const primaryPod = targetPods.find((p) => p.NAME === primaryPodName);
|
|
820
|
+
if (primaryPod) {
|
|
821
|
+
podsToProcess = [primaryPod];
|
|
822
|
+
logger.info('Using MongoDB primary pod', { primaryPod: primaryPodName });
|
|
823
|
+
} else {
|
|
824
|
+
logger.warn('Primary pod not in filtered list, using first pod', { primaryPodName });
|
|
825
|
+
podsToProcess = [targetPods[0]];
|
|
826
|
+
}
|
|
801
827
|
} else {
|
|
802
|
-
logger.warn('
|
|
828
|
+
logger.warn('Could not detect primary pod, using first pod');
|
|
803
829
|
podsToProcess = [targetPods[0]];
|
|
804
830
|
}
|
|
805
|
-
} else {
|
|
806
|
-
logger.warn('Could not detect primary pod, using first pod');
|
|
807
|
-
podsToProcess = [targetPods[0]];
|
|
808
831
|
}
|
|
832
|
+
} else {
|
|
833
|
+
// For MariaDB or when allPods is true, limit to first pod unless allPods is true
|
|
834
|
+
podsToProcess = options.allPods === true ? targetPods : [targetPods[0]];
|
|
809
835
|
}
|
|
810
|
-
} else {
|
|
811
|
-
// For MariaDB or when allPods is true, limit to first pod unless allPods is true
|
|
812
|
-
podsToProcess = options.allPods === true ? targetPods : [targetPods[0]];
|
|
813
|
-
}
|
|
814
836
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
837
|
+
logger.info(`Processing ${podsToProcess.length} pod(s) for ${provider}`, {
|
|
838
|
+
dbName,
|
|
839
|
+
pods: podsToProcess.map((p) => p.NAME),
|
|
840
|
+
});
|
|
819
841
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
842
|
+
// Process each pod
|
|
843
|
+
for (const pod of podsToProcess) {
|
|
844
|
+
logger.info('Processing pod', { podName: pod.NAME, node: pod.NODE, status: pod.STATUS });
|
|
845
|
+
|
|
846
|
+
switch (provider) {
|
|
847
|
+
case 'mariadb': {
|
|
848
|
+
if (options.stats === true) {
|
|
849
|
+
const stats = Underpost.db._getMariaDBStats({
|
|
850
|
+
podName: pod.NAME,
|
|
851
|
+
namespace,
|
|
852
|
+
dbName,
|
|
853
|
+
user,
|
|
854
|
+
password,
|
|
855
|
+
});
|
|
856
|
+
if (stats) {
|
|
857
|
+
Underpost.db._displayStats({ provider, dbName, stats });
|
|
858
|
+
}
|
|
836
859
|
}
|
|
837
|
-
}
|
|
838
860
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
861
|
+
if (options.import === true) {
|
|
862
|
+
Underpost.db._importMariaDB({
|
|
863
|
+
pod,
|
|
864
|
+
namespace,
|
|
865
|
+
dbName,
|
|
866
|
+
user,
|
|
867
|
+
password,
|
|
868
|
+
sqlPath: toSqlPath,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
849
871
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
872
|
+
if (options.export === true) {
|
|
873
|
+
const outputPath = options.outPath || toNewSqlPath;
|
|
874
|
+
await Underpost.db._exportMariaDB({
|
|
875
|
+
pod,
|
|
876
|
+
namespace,
|
|
877
|
+
dbName,
|
|
878
|
+
user,
|
|
879
|
+
password,
|
|
880
|
+
outputPath,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
break;
|
|
860
884
|
}
|
|
861
|
-
break;
|
|
862
|
-
}
|
|
863
885
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
886
|
+
case 'mongoose': {
|
|
887
|
+
if (options.stats === true) {
|
|
888
|
+
const stats = Underpost.db._getMongoStats({
|
|
889
|
+
podName: pod.NAME,
|
|
890
|
+
namespace,
|
|
891
|
+
dbName,
|
|
892
|
+
});
|
|
893
|
+
if (stats) {
|
|
894
|
+
Underpost.db._displayStats({ provider, dbName, stats });
|
|
895
|
+
}
|
|
873
896
|
}
|
|
874
|
-
}
|
|
875
897
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
898
|
+
if (options.import === true) {
|
|
899
|
+
const bsonPath = options.outPath || toBsonPath;
|
|
900
|
+
Underpost.db._importMongoDB({
|
|
901
|
+
pod,
|
|
902
|
+
namespace,
|
|
903
|
+
dbName,
|
|
904
|
+
bsonPath,
|
|
905
|
+
drop: options.drop,
|
|
906
|
+
preserveUUID: options.preserveUUID,
|
|
907
|
+
});
|
|
908
|
+
}
|
|
887
909
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
910
|
+
if (options.export === true) {
|
|
911
|
+
const outputPath = options.outPath || toNewBsonPath;
|
|
912
|
+
Underpost.db._exportMongoDB({
|
|
913
|
+
pod,
|
|
914
|
+
namespace,
|
|
915
|
+
dbName,
|
|
916
|
+
outputPath,
|
|
917
|
+
collections: options.collections,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
break;
|
|
897
921
|
}
|
|
898
|
-
break;
|
|
899
|
-
}
|
|
900
922
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
923
|
+
default:
|
|
924
|
+
logger.warn('Unsupported database provider', { provider });
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
904
927
|
}
|
|
928
|
+
|
|
929
|
+
// Mark this host+path combination as processed
|
|
930
|
+
processedHostPaths.add(hostPathKey);
|
|
905
931
|
}
|
|
932
|
+
}
|
|
906
933
|
|
|
907
|
-
|
|
908
|
-
|
|
934
|
+
// Commit and push to Git if enabled - execute only once per repository
|
|
935
|
+
if (options.export === true && options.git === true && !processedRepos.has(`${repoName}-committed`)) {
|
|
936
|
+
const commitMessage = `${new Date(newBackupTimestamp).toLocaleDateString()} ${new Date(
|
|
937
|
+
newBackupTimestamp,
|
|
938
|
+
).toLocaleTimeString()}`;
|
|
939
|
+
Underpost.repo.manageBackupRepo({ repoName, operation: 'commit', message: commitMessage });
|
|
940
|
+
Underpost.repo.manageBackupRepo({ repoName, operation: 'push' });
|
|
941
|
+
processedRepos.add(`${repoName}-committed`);
|
|
909
942
|
}
|
|
910
943
|
}
|
|
911
944
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
).toLocaleTimeString()}`;
|
|
917
|
-
Underpost.repo.manageBackupRepo({ repoName, operation: 'commit', message: commitMessage });
|
|
918
|
-
Underpost.repo.manageBackupRepo({ repoName, operation: 'push' });
|
|
919
|
-
processedRepos.add(`${repoName}-committed`);
|
|
920
|
-
}
|
|
945
|
+
logger.info('Database operation completed successfully');
|
|
946
|
+
} catch (error) {
|
|
947
|
+
logger.error('Database operation failed', { error: error.message });
|
|
948
|
+
throw error;
|
|
921
949
|
}
|
|
922
|
-
|
|
923
|
-
logger.info('Database operation completed successfully');
|
|
924
950
|
},
|
|
925
951
|
|
|
926
952
|
/**
|
|
@@ -932,6 +958,8 @@ class UnderpostDB {
|
|
|
932
958
|
* @param {string} [deployId=process.env.DEFAULT_DEPLOY_ID] - The deployment ID.
|
|
933
959
|
* @param {string} [host=process.env.DEFAULT_DEPLOY_HOST] - The host identifier.
|
|
934
960
|
* @param {string} [path=process.env.DEFAULT_DEPLOY_PATH] - The path identifier.
|
|
961
|
+
* @param {object} [options] - Options.
|
|
962
|
+
* @param {boolean} [options.dev=false] - Development mode flag.
|
|
935
963
|
* @return {Promise<void>} Resolves when metadata creation is complete.
|
|
936
964
|
* @throws {Error} If database configuration is invalid or connection fails.
|
|
937
965
|
*/
|
|
@@ -939,175 +967,181 @@ class UnderpostDB {
|
|
|
939
967
|
deployId = process.env.DEFAULT_DEPLOY_ID,
|
|
940
968
|
host = process.env.DEFAULT_DEPLOY_HOST,
|
|
941
969
|
path = process.env.DEFAULT_DEPLOY_PATH,
|
|
970
|
+
options = { dev: false },
|
|
942
971
|
) {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
logger.info('Creating cluster metadata', { deployId, host, path });
|
|
972
|
+
try {
|
|
973
|
+
loadCronDeployEnv();
|
|
974
|
+
deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
|
|
975
|
+
host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
|
|
976
|
+
path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
|
|
949
977
|
|
|
950
|
-
|
|
951
|
-
const deployListPath = './engine-private/deploy/dd.router';
|
|
978
|
+
logger.info('Creating cluster metadata', { deployId, host, path });
|
|
952
979
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
throw new Error(`Deploy router file not found: ${deployListPath}`);
|
|
956
|
-
}
|
|
980
|
+
const env = 'production';
|
|
981
|
+
const deployListPath = './engine-private/deploy/dd.router';
|
|
957
982
|
|
|
958
|
-
|
|
983
|
+
if (!fs.existsSync(deployListPath)) {
|
|
984
|
+
logger.error('Deploy router file not found', { path: deployListPath });
|
|
985
|
+
throw new Error(`Deploy router file not found: ${deployListPath}`);
|
|
986
|
+
}
|
|
959
987
|
|
|
960
|
-
|
|
961
|
-
if (!fs.existsSync(confServerPath)) {
|
|
962
|
-
logger.error('Server configuration not found', { path: confServerPath });
|
|
963
|
-
throw new Error(`Server configuration not found: ${confServerPath}`);
|
|
964
|
-
}
|
|
988
|
+
const deployList = fs.readFileSync(deployListPath, 'utf8').split(',');
|
|
965
989
|
|
|
966
|
-
|
|
990
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
991
|
+
if (!fs.existsSync(confServerPath)) {
|
|
992
|
+
logger.error('Server configuration not found', { path: confServerPath });
|
|
993
|
+
throw new Error(`Server configuration not found: ${confServerPath}`);
|
|
994
|
+
}
|
|
967
995
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
996
|
+
const { db } = loadConfServerJson(confServerPath, { resolve: true })[host][path];
|
|
997
|
+
|
|
998
|
+
const maxRetries = 5;
|
|
999
|
+
const retryDelay = 3000;
|
|
1000
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1001
|
+
try {
|
|
1002
|
+
await DataBaseProvider.load({ apis: ['instance', 'cron'], host, path, db });
|
|
1003
|
+
break;
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
if (attempt === maxRetries) {
|
|
1006
|
+
logger.error('Failed to connect to database after retries', { attempts: maxRetries, error: err.message });
|
|
1007
|
+
throw err;
|
|
1008
|
+
}
|
|
1009
|
+
logger.warn('Database connection failed, retrying...', { attempt, maxRetries, error: err.message });
|
|
1010
|
+
await timer(retryDelay);
|
|
978
1011
|
}
|
|
979
|
-
logger.warn('Database connection failed, retrying...', { attempt, maxRetries, error: err.message });
|
|
980
|
-
await timer(retryDelay);
|
|
981
1012
|
}
|
|
982
|
-
}
|
|
983
1013
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1014
|
+
try {
|
|
1015
|
+
/** @type {import('../api/instance/instance.model.js').InstanceModel} */
|
|
1016
|
+
const Instance = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Instance;
|
|
987
1017
|
|
|
988
|
-
|
|
989
|
-
|
|
1018
|
+
await Instance.deleteMany();
|
|
1019
|
+
logger.info('Cleared existing instance metadata');
|
|
990
1020
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1021
|
+
for (const _deployId of deployList) {
|
|
1022
|
+
const deployId = _deployId.trim();
|
|
1023
|
+
if (!deployId) continue;
|
|
994
1024
|
|
|
995
|
-
|
|
1025
|
+
logger.info('Processing deployment for metadata', { deployId });
|
|
996
1026
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1027
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
1028
|
+
if (!fs.existsSync(confServerPath)) {
|
|
1029
|
+
logger.warn('Configuration not found for deployment', { deployId, path: confServerPath });
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1002
1032
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1033
|
+
const confServer = loadReplicas(deployId, loadConfServerJson(confServerPath, { resolve: true }));
|
|
1034
|
+
const router = await Underpost.deploy.routerFactory(deployId, env);
|
|
1035
|
+
const pathPortAssignmentData = await pathPortAssignmentFactory(deployId, router, confServer);
|
|
1006
1036
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1037
|
+
for (const host of Object.keys(confServer)) {
|
|
1038
|
+
for (const { path, port } of pathPortAssignmentData[host]) {
|
|
1039
|
+
if (!confServer[host][path]) continue;
|
|
1010
1040
|
|
|
1011
|
-
|
|
1041
|
+
const { client, runtime, apis, peer } = confServer[host][path];
|
|
1012
1042
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1043
|
+
// Save main instance
|
|
1044
|
+
{
|
|
1045
|
+
const body = {
|
|
1046
|
+
deployId,
|
|
1047
|
+
host,
|
|
1048
|
+
path,
|
|
1049
|
+
port,
|
|
1050
|
+
client,
|
|
1051
|
+
runtime,
|
|
1052
|
+
apis,
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
logger.info('Saving instance metadata', body);
|
|
1056
|
+
await new Instance(body).save();
|
|
1057
|
+
}
|
|
1024
1058
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1059
|
+
// Save peer instance if exists
|
|
1060
|
+
if (peer) {
|
|
1061
|
+
const body = {
|
|
1062
|
+
deployId,
|
|
1063
|
+
host,
|
|
1064
|
+
path: path === '/' ? '/peer' : `${path}/peer`,
|
|
1065
|
+
port: port + 1,
|
|
1066
|
+
runtime: 'nodejs',
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
logger.info('Saving peer instance metadata', body);
|
|
1070
|
+
await new Instance(body).save();
|
|
1071
|
+
}
|
|
1027
1072
|
}
|
|
1073
|
+
}
|
|
1028
1074
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1075
|
+
// Process additional instances
|
|
1076
|
+
const confInstancesPath = `./engine-private/conf/${deployId}/conf.instances.json`;
|
|
1077
|
+
if (fs.existsSync(confInstancesPath)) {
|
|
1078
|
+
const confInstances = JSON.parse(fs.readFileSync(confInstancesPath, 'utf8'));
|
|
1079
|
+
for (const instance of confInstances) {
|
|
1080
|
+
const { id, host, path, fromPort, metadata } = instance;
|
|
1081
|
+
const { runtime } = metadata;
|
|
1031
1082
|
const body = {
|
|
1032
1083
|
deployId,
|
|
1033
1084
|
host,
|
|
1034
|
-
path
|
|
1035
|
-
port:
|
|
1036
|
-
|
|
1085
|
+
path,
|
|
1086
|
+
port: fromPort,
|
|
1087
|
+
client: id,
|
|
1088
|
+
runtime,
|
|
1037
1089
|
};
|
|
1038
|
-
|
|
1039
|
-
logger.info('Saving peer instance metadata', body);
|
|
1090
|
+
logger.info('Saving additional instance metadata', body);
|
|
1040
1091
|
await new Instance(body).save();
|
|
1041
1092
|
}
|
|
1042
1093
|
}
|
|
1043
1094
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
if (fs.existsSync(confInstancesPath)) {
|
|
1048
|
-
const confInstances = JSON.parse(fs.readFileSync(confInstancesPath, 'utf8'));
|
|
1049
|
-
for (const instance of confInstances) {
|
|
1050
|
-
const { id, host, path, fromPort, metadata } = instance;
|
|
1051
|
-
const { runtime } = metadata;
|
|
1052
|
-
const body = {
|
|
1053
|
-
deployId,
|
|
1054
|
-
host,
|
|
1055
|
-
path,
|
|
1056
|
-
port: fromPort,
|
|
1057
|
-
client: id,
|
|
1058
|
-
runtime,
|
|
1059
|
-
};
|
|
1060
|
-
logger.info('Saving additional instance metadata', body);
|
|
1061
|
-
await new Instance(body).save();
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
logger.error('Failed to create instance metadata', { error: error.message });
|
|
1097
|
+
throw error;
|
|
1064
1098
|
}
|
|
1065
|
-
} catch (error) {
|
|
1066
|
-
logger.error('Failed to create instance metadata', { error: error.message });
|
|
1067
|
-
throw error;
|
|
1068
|
-
}
|
|
1069
1099
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1100
|
+
try {
|
|
1101
|
+
const cronDeployPath = './engine-private/deploy/dd.cron';
|
|
1102
|
+
if (!fs.existsSync(cronDeployPath)) {
|
|
1103
|
+
logger.warn('Cron deploy file not found', { path: cronDeployPath });
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1076
1106
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1107
|
+
const cronDeployId = fs.readFileSync(cronDeployPath, 'utf8').trim();
|
|
1108
|
+
const confCronPath = `./engine-private/conf/${cronDeployId}/conf.cron.json`;
|
|
1079
1109
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1110
|
+
if (!fs.existsSync(confCronPath)) {
|
|
1111
|
+
logger.warn('Cron configuration not found', { path: confCronPath });
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1084
1114
|
|
|
1085
|
-
|
|
1115
|
+
const confCron = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
|
|
1086
1116
|
|
|
1087
|
-
|
|
1117
|
+
await DataBaseProvider.load({ apis: ['cron'], host, path, db });
|
|
1088
1118
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1119
|
+
/** @type {import('../api/cron/cron.model.js').CronModel} */
|
|
1120
|
+
const Cron = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Cron;
|
|
1091
1121
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1122
|
+
await Cron.deleteMany();
|
|
1123
|
+
logger.info('Cleared existing cron metadata');
|
|
1094
1124
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1125
|
+
for (const jobId of Object.keys(confCron.jobs)) {
|
|
1126
|
+
const body = {
|
|
1127
|
+
jobId,
|
|
1128
|
+
deployId: Underpost.cron.getRelatedDeployIdList(jobId),
|
|
1129
|
+
expression: confCron.jobs[jobId].expression,
|
|
1130
|
+
enabled: confCron.jobs[jobId].enabled,
|
|
1131
|
+
};
|
|
1132
|
+
logger.info('Saving cron metadata', body);
|
|
1133
|
+
await new Cron(body).save();
|
|
1134
|
+
}
|
|
1135
|
+
} catch (error) {
|
|
1136
|
+
logger.error('Failed to create cron metadata', { error: error.message });
|
|
1104
1137
|
}
|
|
1138
|
+
|
|
1139
|
+
await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
|
|
1140
|
+
logger.info('Cluster metadata creation completed');
|
|
1105
1141
|
} catch (error) {
|
|
1106
|
-
logger.error('
|
|
1142
|
+
logger.error('Cluster metadata creation failed', { error: error.message });
|
|
1143
|
+
throw error;
|
|
1107
1144
|
}
|
|
1108
|
-
|
|
1109
|
-
await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
|
|
1110
|
-
logger.info('Cluster metadata creation completed');
|
|
1111
1145
|
},
|
|
1112
1146
|
|
|
1113
1147
|
/**
|
|
@@ -1121,6 +1155,7 @@ class UnderpostDB {
|
|
|
1121
1155
|
* @param {string} [options.hosts=''] - Comma-separated list of hosts to filter.
|
|
1122
1156
|
* @param {string} [options.paths=''] - Comma-separated list of paths to filter.
|
|
1123
1157
|
* @param {boolean} [options.dryRun=false] - If true, only reports what would be deleted.
|
|
1158
|
+
* @param {boolean} [options.dev=false] - Development mode flag.
|
|
1124
1159
|
* @return {Promise<void>} Resolves when clean operation is complete.
|
|
1125
1160
|
*/
|
|
1126
1161
|
async cleanFsCollection(
|
|
@@ -1129,213 +1164,220 @@ class UnderpostDB {
|
|
|
1129
1164
|
hosts: '',
|
|
1130
1165
|
paths: '',
|
|
1131
1166
|
dryRun: false,
|
|
1167
|
+
dev: false,
|
|
1132
1168
|
},
|
|
1133
1169
|
) {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1170
|
+
const firstDeployId = deployList !== 'dd' ? deployList.split(',')[0].trim() : '';
|
|
1171
|
+
try {
|
|
1172
|
+
loadCronDeployEnv();
|
|
1173
|
+
if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
|
|
1138
1174
|
|
|
1139
|
-
|
|
1140
|
-
const fileRefPath = './src/api/file/file.ref.json';
|
|
1141
|
-
if (!fs.existsSync(fileRefPath)) {
|
|
1142
|
-
logger.error('file.ref.json not found', { path: fileRefPath });
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1175
|
+
logger.info('Starting File collection cleanup', { deployList, options });
|
|
1145
1176
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1177
|
+
// Load file.ref.json to know which models reference File
|
|
1178
|
+
const fileRefPath = './src/api/file/file.ref.json';
|
|
1179
|
+
if (!fs.existsSync(fileRefPath)) {
|
|
1180
|
+
logger.error('file.ref.json not found', { path: fileRefPath });
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1148
1183
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
const filterPaths = options.paths ? options.paths.split(',').map((p) => p.trim()) : [];
|
|
1184
|
+
const fileRefData = JSON.parse(fs.readFileSync(fileRefPath, 'utf8'));
|
|
1185
|
+
logger.info('Loaded file reference configuration', { apis: fileRefData.length });
|
|
1152
1186
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1187
|
+
// Filter hosts and paths if specified
|
|
1188
|
+
const filterHosts = options.hosts ? options.hosts.split(',').map((h) => h.trim()) : [];
|
|
1189
|
+
const filterPaths = options.paths ? options.paths.split(',').map((p) => p.trim()) : [];
|
|
1155
1190
|
|
|
1156
|
-
|
|
1157
|
-
const
|
|
1158
|
-
if (!deployId) continue;
|
|
1191
|
+
// Track all connections to close them at the end
|
|
1192
|
+
const connectionsToClose = [];
|
|
1159
1193
|
|
|
1160
|
-
|
|
1194
|
+
for (const _deployId of deployList.split(',')) {
|
|
1195
|
+
const deployId = _deployId.trim();
|
|
1196
|
+
if (!deployId) continue;
|
|
1161
1197
|
|
|
1162
|
-
|
|
1163
|
-
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
1164
|
-
if (!fs.existsSync(confServerPath)) {
|
|
1165
|
-
logger.error('Configuration file not found', { path: confServerPath });
|
|
1166
|
-
continue;
|
|
1167
|
-
}
|
|
1198
|
+
logger.info('Processing deployment for File cleanup', { deployId });
|
|
1168
1199
|
|
|
1169
|
-
|
|
1200
|
+
// Load server configuration
|
|
1201
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
1202
|
+
if (!fs.existsSync(confServerPath)) {
|
|
1203
|
+
logger.error('Configuration file not found', { path: confServerPath });
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1170
1206
|
|
|
1171
|
-
|
|
1172
|
-
for (const host of Object.keys(confServer)) {
|
|
1173
|
-
if (filterHosts.length > 0 && !filterHosts.includes(host)) continue;
|
|
1207
|
+
const confServer = loadConfServerJson(confServerPath, { resolve: true });
|
|
1174
1208
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1209
|
+
// Process each host+path combination
|
|
1210
|
+
for (const host of Object.keys(confServer)) {
|
|
1211
|
+
if (filterHosts.length > 0 && !filterHosts.includes(host)) continue;
|
|
1177
1212
|
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1213
|
+
for (const path of Object.keys(confServer[host])) {
|
|
1214
|
+
if (filterPaths.length > 0 && !filterPaths.includes(path)) continue;
|
|
1180
1215
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
logger.info('Skipping - no file api in configuration', { host, path });
|
|
1184
|
-
continue;
|
|
1185
|
-
}
|
|
1216
|
+
const { db, apis } = confServer[host][path];
|
|
1217
|
+
if (!db || !apis) continue;
|
|
1186
1218
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
// Connect to database with retry
|
|
1191
|
-
let dbProvider;
|
|
1192
|
-
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
1193
|
-
try {
|
|
1194
|
-
dbProvider = await DataBaseProvider.load({ apis, host, path, db });
|
|
1195
|
-
break;
|
|
1196
|
-
} catch (err) {
|
|
1197
|
-
if (attempt === 3) throw err;
|
|
1198
|
-
logger.warn('Database connection failed, retrying...', { attempt, host, path, error: err.message });
|
|
1199
|
-
await timer(3000);
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
if (!dbProvider || !dbProvider.models) {
|
|
1203
|
-
logger.error('Failed to load database provider', { host, path });
|
|
1219
|
+
// Check if 'file' api is in the apis list
|
|
1220
|
+
if (!apis.includes('file')) {
|
|
1221
|
+
logger.info('Skipping - no file api in configuration', { host, path });
|
|
1204
1222
|
continue;
|
|
1205
1223
|
}
|
|
1206
1224
|
|
|
1207
|
-
|
|
1225
|
+
// logger.info('Processing host+path with file api', { host, path, db: db.name });
|
|
1226
|
+
|
|
1227
|
+
try {
|
|
1228
|
+
// Connect to database with retry
|
|
1229
|
+
let dbProvider;
|
|
1230
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
1231
|
+
try {
|
|
1232
|
+
dbProvider = await DataBaseProvider.load({ apis, host, path, db });
|
|
1233
|
+
break;
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
if (attempt === 3) throw err;
|
|
1236
|
+
logger.warn('Database connection failed, retrying...', { attempt, host, path, error: err.message });
|
|
1237
|
+
await timer(3000);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (!dbProvider || !dbProvider.models) {
|
|
1241
|
+
logger.error('Failed to load database provider', { host, path });
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1208
1244
|
|
|
1209
|
-
|
|
1210
|
-
connectionsToClose.push({ host, path, dbProvider });
|
|
1245
|
+
const { models } = dbProvider;
|
|
1211
1246
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
logger.warn('File model not loaded', { host, path });
|
|
1215
|
-
continue;
|
|
1216
|
-
}
|
|
1247
|
+
// Track this connection for cleanup
|
|
1248
|
+
connectionsToClose.push({ host, path, dbProvider });
|
|
1217
1249
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1250
|
+
// Check if File model exists
|
|
1251
|
+
if (!models.File) {
|
|
1252
|
+
logger.warn('File model not loaded', { host, path });
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1221
1255
|
|
|
1222
|
-
|
|
1256
|
+
// Get all File documents
|
|
1257
|
+
const allFiles = await models.File.find({}, '_id').lean();
|
|
1258
|
+
logger.info('Found File documents', { count: allFiles.length, host, path });
|
|
1223
1259
|
|
|
1224
|
-
|
|
1225
|
-
const referencedFileIds = new Set();
|
|
1260
|
+
if (allFiles.length === 0) continue;
|
|
1226
1261
|
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
const { api, model: modelFields } = refConfig;
|
|
1262
|
+
// Track which File IDs are referenced
|
|
1263
|
+
const referencedFileIds = new Set();
|
|
1230
1264
|
|
|
1231
|
-
// Check
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1234
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1235
|
-
.join('');
|
|
1236
|
-
const Model = models[modelName];
|
|
1265
|
+
// Check each API from file.ref.json
|
|
1266
|
+
for (const refConfig of fileRefData) {
|
|
1267
|
+
const { api, model: modelFields } = refConfig;
|
|
1237
1268
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1269
|
+
// Check if this API is loaded in current context
|
|
1270
|
+
const modelName = api
|
|
1271
|
+
.split('-')
|
|
1272
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1273
|
+
.join('');
|
|
1274
|
+
const Model = models[modelName];
|
|
1275
|
+
|
|
1276
|
+
if (!Model) {
|
|
1277
|
+
logger.debug('Model not loaded in current context', { api, modelName, host, path });
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1242
1280
|
|
|
1243
|
-
|
|
1281
|
+
logger.info('Checking references in model', { api, modelName });
|
|
1244
1282
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1283
|
+
// Helper function to recursively check field references
|
|
1284
|
+
const checkFieldReferences = async (fieldPath, fieldConfig) => {
|
|
1285
|
+
for (const [fieldName, fieldValue] of Object.entries(fieldConfig)) {
|
|
1286
|
+
const currentPath = fieldPath ? `${fieldPath}.${fieldName}` : fieldName;
|
|
1249
1287
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1288
|
+
if (fieldValue === true) {
|
|
1289
|
+
// This is a File reference field
|
|
1290
|
+
const query = {};
|
|
1291
|
+
query[currentPath] = { $exists: true, $ne: null };
|
|
1254
1292
|
|
|
1255
|
-
|
|
1293
|
+
const docs = await Model.find(query, currentPath).lean();
|
|
1256
1294
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1295
|
+
for (const doc of docs) {
|
|
1296
|
+
// Navigate to the nested field
|
|
1297
|
+
const parts = currentPath.split('.');
|
|
1298
|
+
let value = doc;
|
|
1299
|
+
for (const part of parts) {
|
|
1300
|
+
value = value?.[part];
|
|
1301
|
+
}
|
|
1264
1302
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1303
|
+
if (value) {
|
|
1304
|
+
if (Array.isArray(value)) {
|
|
1305
|
+
value.forEach((id) => id && referencedFileIds.add(id.toString()));
|
|
1306
|
+
} else {
|
|
1307
|
+
referencedFileIds.add(value.toString());
|
|
1308
|
+
}
|
|
1270
1309
|
}
|
|
1271
1310
|
}
|
|
1272
|
-
}
|
|
1273
1311
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1312
|
+
logger.info('Found references', {
|
|
1313
|
+
model: modelName,
|
|
1314
|
+
field: currentPath,
|
|
1315
|
+
count: docs.length,
|
|
1316
|
+
});
|
|
1317
|
+
} else if (typeof fieldValue === 'object') {
|
|
1318
|
+
// Nested object, recurse
|
|
1319
|
+
await checkFieldReferences(currentPath, fieldValue);
|
|
1320
|
+
}
|
|
1282
1321
|
}
|
|
1283
|
-
}
|
|
1284
|
-
};
|
|
1322
|
+
};
|
|
1285
1323
|
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
logger.info('Total referenced File IDs', { count: referencedFileIds.size, host, path });
|
|
1324
|
+
await checkFieldReferences('', modelFields);
|
|
1325
|
+
}
|
|
1290
1326
|
|
|
1291
|
-
|
|
1292
|
-
const orphanedFiles = allFiles.filter((file) => !referencedFileIds.has(file._id.toString()));
|
|
1327
|
+
logger.info('Total referenced File IDs', { count: referencedFileIds.size, host, path });
|
|
1293
1328
|
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
} else {
|
|
1297
|
-
logger.info('Found orphaned files', { count: orphanedFiles.length, host, path });
|
|
1329
|
+
// Find orphaned files
|
|
1330
|
+
const orphanedFiles = allFiles.filter((file) => !referencedFileIds.has(file._id.toString()));
|
|
1298
1331
|
|
|
1299
|
-
if (
|
|
1300
|
-
logger.info('
|
|
1301
|
-
count: orphanedFiles.length,
|
|
1302
|
-
ids: orphanedFiles.map((f) => f._id.toString()),
|
|
1303
|
-
});
|
|
1332
|
+
if (orphanedFiles.length === 0) {
|
|
1333
|
+
logger.info('No orphaned files found', { host, path });
|
|
1304
1334
|
} else {
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1335
|
+
logger.info('Found orphaned files', { count: orphanedFiles.length, host, path });
|
|
1336
|
+
|
|
1337
|
+
if (options.dryRun) {
|
|
1338
|
+
logger.info('Dry run - would delete files', {
|
|
1339
|
+
count: orphanedFiles.length,
|
|
1340
|
+
ids: orphanedFiles.map((f) => f._id.toString()),
|
|
1341
|
+
});
|
|
1342
|
+
} else {
|
|
1343
|
+
const orphanedIds = orphanedFiles.map((f) => f._id);
|
|
1344
|
+
const deleteResult = await models.File.deleteMany({ _id: { $in: orphanedIds } });
|
|
1345
|
+
logger.info('Deleted orphaned files', {
|
|
1346
|
+
deletedCount: deleteResult.deletedCount,
|
|
1347
|
+
host,
|
|
1348
|
+
path,
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1312
1351
|
}
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
logger.error('Error processing host+path', {
|
|
1354
|
+
host,
|
|
1355
|
+
path,
|
|
1356
|
+
error: error.message,
|
|
1357
|
+
});
|
|
1313
1358
|
}
|
|
1314
|
-
} catch (error) {
|
|
1315
|
-
logger.error('Error processing host+path', {
|
|
1316
|
-
host,
|
|
1317
|
-
path,
|
|
1318
|
-
error: error.message,
|
|
1319
|
-
});
|
|
1320
1359
|
}
|
|
1321
1360
|
}
|
|
1322
1361
|
}
|
|
1323
|
-
}
|
|
1324
1362
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1363
|
+
// Close all connections
|
|
1364
|
+
logger.info('Closing all database connections', { count: connectionsToClose.length });
|
|
1365
|
+
for (const { host, path, dbProvider } of connectionsToClose) {
|
|
1366
|
+
try {
|
|
1367
|
+
if (dbProvider && dbProvider.close) {
|
|
1368
|
+
await dbProvider.close();
|
|
1369
|
+
logger.info('Connection closed', { host, path });
|
|
1370
|
+
}
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
logger.error('Error closing connection', { host, path, error: error.message });
|
|
1332
1373
|
}
|
|
1333
|
-
} catch (error) {
|
|
1334
|
-
logger.error('Error closing connection', { host, path, error: error.message });
|
|
1335
1374
|
}
|
|
1336
|
-
}
|
|
1337
1375
|
|
|
1338
|
-
|
|
1376
|
+
logger.info('File collection cleanup completed');
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
logger.error('File collection cleanup failed', { error: error.message });
|
|
1379
|
+
throw error;
|
|
1380
|
+
}
|
|
1339
1381
|
},
|
|
1340
1382
|
|
|
1341
1383
|
/**
|
|
@@ -1354,6 +1396,7 @@ class UnderpostDB {
|
|
|
1354
1396
|
* @param {boolean} [options.export=false] - Export metadata to backup.
|
|
1355
1397
|
* @param {boolean} [options.instances=false] - Process instances collection.
|
|
1356
1398
|
* @param {boolean} [options.crons=false] - Process crons collection.
|
|
1399
|
+
* @param {boolean} [options.dev=false] - Development mode flag.
|
|
1357
1400
|
* @return {Promise<void>} Resolves when backup operation is complete.
|
|
1358
1401
|
*/
|
|
1359
1402
|
async clusterMetadataBackupCallback(
|
|
@@ -1367,70 +1410,76 @@ class UnderpostDB {
|
|
|
1367
1410
|
export: false,
|
|
1368
1411
|
instances: false,
|
|
1369
1412
|
crons: false,
|
|
1413
|
+
dev: false,
|
|
1370
1414
|
},
|
|
1371
1415
|
) {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
if (options.generate === true) {
|
|
1385
|
-
logger.info('Generating cluster metadata');
|
|
1386
|
-
await Underpost.db.clusterMetadataFactory(deployId, host, path);
|
|
1387
|
-
}
|
|
1416
|
+
try {
|
|
1417
|
+
loadCronDeployEnv();
|
|
1418
|
+
deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
|
|
1419
|
+
host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
|
|
1420
|
+
path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
|
|
1421
|
+
|
|
1422
|
+
logger.info('Starting cluster metadata backup operation', {
|
|
1423
|
+
deployId,
|
|
1424
|
+
host,
|
|
1425
|
+
path,
|
|
1426
|
+
options,
|
|
1427
|
+
});
|
|
1388
1428
|
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
fs.mkdirSync(outputPath, { recursive: true });
|
|
1429
|
+
if (options.generate === true) {
|
|
1430
|
+
logger.info('Generating cluster metadata');
|
|
1431
|
+
await Underpost.db.clusterMetadataFactory(deployId, host, path);
|
|
1393
1432
|
}
|
|
1394
|
-
const collection = 'instances';
|
|
1395
1433
|
|
|
1396
|
-
if (options.
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1434
|
+
if (options.instances === true) {
|
|
1435
|
+
const outputPath = './engine-private/instances';
|
|
1436
|
+
if (!fs.existsSync(outputPath)) {
|
|
1437
|
+
fs.mkdirSync(outputPath, { recursive: true });
|
|
1438
|
+
}
|
|
1439
|
+
const collection = 'instances';
|
|
1402
1440
|
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
}
|
|
1441
|
+
if (options.export === true) {
|
|
1442
|
+
logger.info('Exporting instances collection', { outputPath });
|
|
1443
|
+
shellExec(
|
|
1444
|
+
`node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1410
1447
|
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1448
|
+
if (options.import === true) {
|
|
1449
|
+
logger.info('Importing instances collection', { outputPath });
|
|
1450
|
+
shellExec(
|
|
1451
|
+
`node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1415
1454
|
}
|
|
1416
|
-
const collection = 'crons';
|
|
1417
1455
|
|
|
1418
|
-
if (options.
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1456
|
+
if (options.crons === true) {
|
|
1457
|
+
const outputPath = './engine-private/crons';
|
|
1458
|
+
if (!fs.existsSync(outputPath)) {
|
|
1459
|
+
fs.mkdirSync(outputPath, { recursive: true });
|
|
1460
|
+
}
|
|
1461
|
+
const collection = 'crons';
|
|
1462
|
+
|
|
1463
|
+
if (options.export === true) {
|
|
1464
|
+
logger.info('Exporting crons collection', { outputPath });
|
|
1465
|
+
shellExec(
|
|
1466
|
+
`node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1424
1469
|
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1470
|
+
if (options.import === true) {
|
|
1471
|
+
logger.info('Importing crons collection', { outputPath });
|
|
1472
|
+
shellExec(
|
|
1473
|
+
`node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1430
1476
|
}
|
|
1431
|
-
}
|
|
1432
1477
|
|
|
1433
|
-
|
|
1478
|
+
logger.info('Cluster metadata backup operation completed');
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
logger.error('Cluster metadata backup operation failed', { error: error.message });
|
|
1481
|
+
throw error;
|
|
1482
|
+
}
|
|
1434
1483
|
},
|
|
1435
1484
|
};
|
|
1436
1485
|
}
|