cyberia 3.0.3 → 3.2.5
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/{.env.production → .env.example} +20 -4
- package/.github/workflows/engine-cyberia.cd.yml +43 -10
- package/.github/workflows/engine-cyberia.ci.yml +48 -26
- package/.github/workflows/ghpkg.ci.yml +5 -5
- package/.github/workflows/gitlab.ci.yml +1 -1
- package/.github/workflows/hardhat.ci.yml +82 -0
- package/.github/workflows/npmpkg.ci.yml +60 -14
- package/.github/workflows/publish.ci.yml +26 -7
- package/.github/workflows/publish.cyberia.ci.yml +5 -5
- package/.github/workflows/pwa-microservices-template-page.cd.yml +6 -7
- package/.github/workflows/pwa-microservices-template-test.ci.yml +4 -4
- package/.github/workflows/release.cd.yml +14 -8
- package/.vscode/extensions.json +9 -8
- package/.vscode/settings.json +3 -2
- package/CHANGELOG.md +643 -1
- package/CLI-HELP.md +132 -57
- package/Dockerfile +4 -2
- package/README.md +347 -22
- package/WHITE-PAPER.md +1540 -0
- package/bin/build.js +21 -12
- package/bin/cyberia.js +2640 -106
- package/bin/deploy.js +258 -372
- package/bin/file.js +5 -1
- package/bin/index.js +2640 -106
- package/bin/vs.js +3 -3
- package/conf.js +169 -105
- package/deployment.yaml +236 -20
- package/hardhat/.env.example +31 -0
- package/hardhat/README.md +531 -0
- package/hardhat/WHITE-PAPER.md +1540 -0
- package/hardhat/contracts/ObjectLayerToken.sol +391 -0
- package/hardhat/deployments/.gitkeep +0 -0
- package/hardhat/deployments/hardhat-ObjectLayerToken.json +11 -0
- package/hardhat/hardhat.config.js +136 -0
- package/hardhat/ignition/modules/ObjectLayerToken.js +21 -0
- package/hardhat/networks/besu-object-layer.network.json +138 -0
- package/hardhat/package-lock.json +4323 -0
- package/hardhat/package.json +36 -0
- package/hardhat/scripts/deployObjectLayerToken.js +98 -0
- package/hardhat/test/ObjectLayerToken.js +592 -0
- package/hardhat/types/ethers-contracts/ObjectLayerToken.ts +690 -0
- package/hardhat/types/ethers-contracts/common.ts +92 -0
- package/hardhat/types/ethers-contracts/factories/ObjectLayerToken__factory.ts +1055 -0
- package/hardhat/types/ethers-contracts/factories/index.ts +4 -0
- package/hardhat/types/ethers-contracts/hardhat.d.ts +47 -0
- package/hardhat/types/ethers-contracts/index.ts +6 -0
- package/jsdoc.dd-cyberia.json +68 -0
- package/jsdoc.json +65 -49
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -4
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +5 -4
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +562 -0
- package/manifests/deployment/dd-cyberia-development/proxy.yaml +297 -0
- package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +132 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +88 -74
- package/manifests/deployment/dd-test-development/proxy.yaml +13 -4
- package/manifests/deployment/playwright/deployment.yaml +1 -1
- package/manifests/pv-pvc-dd.yaml +1 -1
- package/nodemon.json +1 -1
- package/package.json +60 -48
- package/proxy.yaml +118 -10
- package/pv-pvc.yaml +132 -0
- package/scripts/k3s-node-setup.sh +1 -1
- package/scripts/ports-ls.sh +2 -0
- package/scripts/rhel-grpc-setup.sh +56 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +47 -1
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +17 -2
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +80 -7
- package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +93 -0
- package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +36 -0
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +29 -0
- package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +51 -0
- package/src/api/cyberia-entity/cyberia-entity.controller.js +74 -0
- package/src/api/cyberia-entity/cyberia-entity.model.js +24 -0
- package/src/api/cyberia-entity/cyberia-entity.router.js +27 -0
- package/src/api/cyberia-entity/cyberia-entity.service.js +42 -0
- package/src/api/cyberia-instance/cyberia-fallback-world.js +368 -0
- package/src/api/cyberia-instance/cyberia-instance.controller.js +92 -0
- package/src/api/cyberia-instance/cyberia-instance.model.js +84 -0
- package/src/api/cyberia-instance/cyberia-instance.router.js +63 -0
- package/src/api/cyberia-instance/cyberia-instance.service.js +191 -0
- package/src/api/cyberia-instance/cyberia-portal-connector.js +486 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +74 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +413 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +228 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +27 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +42 -0
- package/src/api/cyberia-map/cyberia-map.controller.js +79 -0
- package/src/api/cyberia-map/cyberia-map.model.js +30 -0
- package/src/api/cyberia-map/cyberia-map.router.js +40 -0
- package/src/api/cyberia-map/cyberia-map.service.js +74 -0
- package/src/api/document/document.service.js +1 -1
- package/src/api/file/file.controller.js +3 -1
- package/src/api/file/file.ref.json +18 -0
- package/src/api/file/file.service.js +28 -5
- package/src/api/ipfs/ipfs.controller.js +4 -25
- package/src/api/ipfs/ipfs.model.js +43 -34
- package/src/api/ipfs/ipfs.router.js +8 -13
- package/src/api/ipfs/ipfs.service.js +56 -104
- package/src/api/object-layer/README.md +347 -22
- package/src/api/object-layer/object-layer.controller.js +6 -2
- package/src/api/object-layer/object-layer.model.js +12 -8
- package/src/api/object-layer/object-layer.router.js +698 -42
- package/src/api/object-layer/object-layer.service.js +119 -37
- package/src/api/object-layer-render-frames/object-layer-render-frames.model.js +1 -2
- package/src/api/user/user.router.js +10 -5
- package/src/api/user/user.service.js +15 -14
- package/src/cli/baremetal.js +6 -10
- package/src/cli/cloud-init.js +0 -3
- package/src/cli/cluster.js +7 -7
- package/src/cli/db.js +723 -857
- package/src/cli/deploy.js +215 -105
- package/src/cli/env.js +34 -5
- package/src/cli/fs.js +5 -4
- package/src/cli/image.js +0 -3
- package/src/cli/index.js +83 -15
- package/src/cli/kubectl.js +211 -0
- package/src/cli/monitor.js +5 -6
- package/src/cli/release.js +284 -0
- package/src/cli/repository.js +708 -62
- package/src/cli/run.js +371 -151
- package/src/cli/secrets.js +73 -2
- package/src/cli/ssh.js +1 -1
- package/src/cli/test.js +3 -3
- package/src/client/Cryptokoyn.index.js +3 -4
- package/src/client/CyberiaPortal.index.js +3 -4
- package/src/client/Default.index.js +3 -4
- package/src/client/Itemledger.index.js +4 -963
- package/src/client/Underpost.index.js +3 -4
- package/src/client/components/core/AgGrid.js +20 -5
- package/src/client/components/core/Alert.js +2 -2
- package/src/client/components/core/AppStore.js +69 -0
- package/src/client/components/core/CalendarCore.js +2 -2
- package/src/client/components/core/Content.js +22 -3
- package/src/client/components/core/Docs.js +30 -6
- package/src/client/components/core/DropDown.js +137 -17
- package/src/client/components/core/FileExplorer.js +71 -4
- package/src/client/components/core/Input.js +1 -1
- package/src/client/components/core/Keyboard.js +2 -2
- package/src/client/components/core/LogIn.js +2 -2
- package/src/client/components/core/LogOut.js +2 -2
- package/src/client/components/core/Modal.js +20 -7
- package/src/client/components/core/Panel.js +0 -1
- package/src/client/components/core/PanelForm.js +19 -19
- package/src/client/components/core/RichText.js +1 -2
- package/src/client/components/core/SocketIo.js +82 -29
- package/src/client/components/core/SocketIoHandler.js +75 -0
- package/src/client/components/core/Stream.js +143 -95
- package/src/client/components/core/Webhook.js +40 -7
- package/src/client/components/cryptokoyn/AppStoreCryptokoyn.js +5 -0
- package/src/client/components/cryptokoyn/LogInCryptokoyn.js +3 -3
- package/src/client/components/cryptokoyn/LogOutCryptokoyn.js +2 -2
- package/src/client/components/cryptokoyn/MenuCryptokoyn.js +3 -3
- package/src/client/components/cryptokoyn/SocketIoCryptokoyn.js +3 -51
- package/src/client/components/cyberia/InstanceEngineCyberia.js +700 -0
- package/src/client/components/cyberia/MapEngineCyberia.js +1359 -2
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +17 -6
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +92 -54
- package/src/client/components/cyberia-portal/AppStoreCyberiaPortal.js +5 -0
- package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +217 -30
- package/src/client/components/cyberia-portal/CssCyberiaPortal.js +44 -2
- package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +3 -4
- package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +2 -2
- package/src/client/components/cyberia-portal/MenuCyberiaPortal.js +104 -9
- package/src/client/components/cyberia-portal/RoutesCyberiaPortal.js +5 -0
- package/src/client/components/cyberia-portal/SocketIoCyberiaPortal.js +3 -49
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
- package/src/client/components/default/AppStoreDefault.js +5 -0
- package/src/client/components/default/LogInDefault.js +3 -3
- package/src/client/components/default/LogOutDefault.js +2 -2
- package/src/client/components/default/MenuDefault.js +5 -5
- package/src/client/components/default/SocketIoDefault.js +3 -51
- package/src/client/components/itemledger/AppStoreItemledger.js +5 -0
- package/src/client/components/itemledger/LogInItemledger.js +3 -3
- package/src/client/components/itemledger/LogOutItemledger.js +2 -2
- package/src/client/components/itemledger/MenuItemledger.js +3 -3
- package/src/client/components/itemledger/SocketIoItemledger.js +3 -51
- package/src/client/components/underpost/AppStoreUnderpost.js +5 -0
- package/src/client/components/underpost/CssUnderpost.js +59 -0
- package/src/client/components/underpost/LogInUnderpost.js +6 -3
- package/src/client/components/underpost/LogOutUnderpost.js +4 -2
- package/src/client/components/underpost/MenuUnderpost.js +104 -18
- package/src/client/components/underpost/RoutesUnderpost.js +2 -0
- package/src/client/components/underpost/SocketIoUnderpost.js +3 -51
- package/src/client/public/cryptokoyn/assets/logo/base-icon.png +0 -0
- package/src/client/public/cryptokoyn/browserconfig.xml +12 -0
- package/src/client/public/cryptokoyn/microdata.json +85 -0
- package/src/client/public/cryptokoyn/site.webmanifest +57 -0
- package/src/client/public/cryptokoyn/sitemap +3 -3
- package/src/client/public/default/sitemap +3 -3
- package/src/client/public/itemledger/browserconfig.xml +2 -2
- package/src/client/public/itemledger/manifest.webmanifest +4 -4
- package/src/client/public/itemledger/microdata.json +71 -0
- package/src/client/public/itemledger/sitemap +3 -3
- package/src/client/public/itemledger/yandex-browser-manifest.json +2 -2
- package/src/client/public/test/sitemap +3 -3
- package/src/client/services/core/core.service.js +20 -8
- package/src/client/services/cyberia-dialogue/cyberia-dialogue.service.js +105 -0
- package/src/client/services/cyberia-entity/cyberia-entity.management.js +57 -0
- package/src/client/services/cyberia-entity/cyberia-entity.service.js +105 -0
- package/src/client/services/cyberia-instance/cyberia-instance.management.js +194 -0
- package/src/client/services/cyberia-instance/cyberia-instance.service.js +122 -0
- package/src/client/services/cyberia-instance-conf/cyberia-instance-conf.service.js +105 -0
- package/src/client/services/cyberia-map/cyberia-map.management.js +193 -0
- package/src/client/services/cyberia-map/cyberia-map.service.js +126 -0
- package/src/client/services/instance/instance.management.js +2 -2
- package/src/client/services/ipfs/ipfs.service.js +3 -23
- package/src/client/services/object-layer/object-layer.management.js +3 -3
- package/src/client/services/object-layer/object-layer.service.js +21 -0
- package/src/client/services/user/user.management.js +2 -2
- package/src/client/ssr/body/404.js +15 -11
- package/src/client/ssr/body/500.js +15 -11
- package/src/client/ssr/body/SwaggerDarkMode.js +285 -0
- package/src/client/ssr/head/PwaItemledger.js +60 -0
- package/src/client/ssr/offline/NoNetworkConnection.js +11 -10
- package/src/client/ssr/pages/CyberiaServerMetrics.js +1 -1
- package/src/client/ssr/pages/Test.js +11 -10
- package/src/client.build.js +0 -3
- package/src/client.dev.js +0 -3
- package/src/db/DataBaseProvider.js +17 -2
- package/src/db/mariadb/MariaDB.js +14 -9
- package/src/db/mongo/MongooseDB.js +17 -1
- package/src/grpc/cyberia/OFF_CHAIN_ECONOMY.md +305 -0
- package/src/grpc/cyberia/README.md +326 -0
- package/src/grpc/cyberia/grpc-server.js +530 -0
- package/src/index.js +24 -1
- package/src/proxy.js +0 -3
- package/src/runtime/express/Dockerfile +4 -0
- package/src/runtime/express/Express.js +33 -10
- package/src/runtime/lampp/Dockerfile +13 -2
- package/src/runtime/lampp/Lampp.js +33 -17
- package/src/runtime/wp/Dockerfile +68 -0
- package/src/runtime/wp/Wp.js +639 -0
- package/src/server/auth.js +36 -15
- package/src/server/backup.js +39 -12
- package/src/server/besu-genesis-generator.js +1630 -0
- package/src/server/client-build-docs.js +133 -17
- package/src/server/client-build-live.js +9 -18
- package/src/server/client-build.js +229 -101
- package/src/server/client-dev-server.js +14 -13
- package/src/server/client-formatted.js +109 -57
- package/src/server/conf.js +391 -164
- package/src/server/cron.js +27 -24
- package/src/server/dns.js +29 -12
- package/src/server/downloader.js +0 -2
- package/src/server/ipfs-client.js +24 -1
- package/src/server/logger.js +27 -9
- package/src/server/object-layer.js +217 -103
- package/src/server/peer.js +8 -2
- package/src/server/process.js +1 -50
- package/src/server/proxy.js +4 -8
- package/src/server/runtime.js +30 -9
- package/src/server/semantic-layer-generator-floor.js +359 -0
- package/src/server/semantic-layer-generator-skin.js +1294 -0
- package/src/server/semantic-layer-generator.js +116 -555
- package/src/server/ssr.js +0 -3
- package/src/server/start.js +19 -12
- package/src/server/tls.js +0 -2
- package/src/server.js +0 -4
- package/src/ws/IoInterface.js +1 -10
- package/src/ws/IoServer.js +14 -33
- package/src/ws/core/channels/core.ws.chat.js +65 -20
- package/src/ws/core/channels/core.ws.mailer.js +113 -32
- package/src/ws/core/channels/core.ws.stream.js +90 -31
- package/src/ws/core/core.ws.connection.js +12 -33
- package/src/ws/core/core.ws.emit.js +10 -26
- package/src/ws/core/core.ws.server.js +25 -58
- package/src/ws/default/channels/default.ws.main.js +53 -12
- package/src/ws/default/default.ws.connection.js +26 -13
- package/src/ws/default/default.ws.server.js +30 -12
- package/.env.development +0 -43
- package/.env.test +0 -43
- package/hardhat/contracts/CryptoKoyn.sol +0 -59
- package/hardhat/contracts/ItemLedger.sol +0 -73
- package/hardhat/contracts/Lock.sol +0 -34
- package/hardhat/hardhat.config.cjs +0 -45
- package/hardhat/ignition/modules/Lock.js +0 -18
- package/hardhat/networks/cryptokoyn-itemledger.network.json +0 -29
- package/hardhat/scripts/deployCryptokoyn.cjs +0 -25
- package/hardhat/scripts/deployItemledger.cjs +0 -25
- package/hardhat/test/Lock.js +0 -126
- package/hardhat/white-paper.md +0 -581
- package/src/client/components/cryptokoyn/CommonCryptokoyn.js +0 -29
- package/src/client/components/cryptokoyn/ElementsCryptokoyn.js +0 -38
- package/src/client/components/cyberia-portal/ElementsCyberiaPortal.js +0 -38
- package/src/client/components/default/ElementsDefault.js +0 -38
- package/src/client/components/itemledger/CommonItemledger.js +0 -29
- package/src/client/components/itemledger/ElementsItemledger.js +0 -38
- package/src/client/components/underpost/CommonUnderpost.js +0 -29
- package/src/client/components/underpost/ElementsUnderpost.js +0 -38
- package/src/ws/core/management/core.ws.chat.js +0 -8
- package/src/ws/core/management/core.ws.mailer.js +0 -16
- package/src/ws/core/management/core.ws.stream.js +0 -8
- package/src/ws/default/management/default.ws.main.js +0 -8
- package/white-paper.md +0 -581
package/src/cli/db.js
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
* Supports MariaDB and MongoDB with import/export capabilities, Git integration, and multi-pod operations.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { mergeFile, splitFileFactory } from '../server/conf.js';
|
|
9
|
+
import { mergeFile, splitFileFactory, loadConfServerJson, resolveConfSecrets } from '../server/conf.js';
|
|
10
10
|
import { loggerFactory } from '../server/logger.js';
|
|
11
11
|
import { shellExec } from '../server/process.js';
|
|
12
12
|
import fs from 'fs-extra';
|
|
13
13
|
import { DataBaseProvider } from '../db/DataBaseProvider.js';
|
|
14
|
-
import { loadReplicas, pathPortAssignmentFactory } from '../server/conf.js';
|
|
14
|
+
import { loadReplicas, pathPortAssignmentFactory, loadCronDeployEnv } from '../server/conf.js';
|
|
15
15
|
import Underpost from '../index.js';
|
|
16
|
+
import { timer } from '../client/components/core/CommonJs.js';
|
|
16
17
|
const logger = loggerFactory(import.meta);
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -84,244 +85,18 @@ class UnderpostDB {
|
|
|
84
85
|
*/
|
|
85
86
|
static API = {
|
|
86
87
|
/**
|
|
87
|
-
* Helper:
|
|
88
|
-
*
|
|
88
|
+
* Helper: Resolves the latest backup timestamp from an existing backup directory.
|
|
89
|
+
* Scans the directory for numeric (epoch) sub-folders and returns the most recent one.
|
|
90
|
+
* @method _getLatestBackupTimestamp
|
|
89
91
|
* @memberof UnderpostDB
|
|
90
|
-
* @param {
|
|
91
|
-
* @
|
|
92
|
-
* @param {string} [criteria.namespace='default'] - Kubernetes namespace.
|
|
93
|
-
* @param {string} [criteria.deployId] - Deployment ID pattern.
|
|
94
|
-
* @return {Array<PodInfo>} Filtered pod list.
|
|
92
|
+
* @param {string} backupDir - Path to the host-folder backup directory.
|
|
93
|
+
* @return {string|null} The latest timestamp string, or null if none found.
|
|
95
94
|
*/
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
let pods = Underpost.deploy.get(deployId || '', 'pods', namespace);
|
|
102
|
-
|
|
103
|
-
// Filter by pod names if specified
|
|
104
|
-
if (podNames) {
|
|
105
|
-
const patterns = podNames.split(',').map((p) => p.trim());
|
|
106
|
-
pods = pods.filter((pod) => {
|
|
107
|
-
return patterns.some((pattern) => {
|
|
108
|
-
// Support wildcards
|
|
109
|
-
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
110
|
-
return regex.test(pod.NAME);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
logger.info(`Found ${pods.length} pod(s) matching criteria`, { criteria, podNames: pods.map((p) => p.NAME) });
|
|
116
|
-
return pods;
|
|
117
|
-
} catch (error) {
|
|
118
|
-
logger.error('Error filtering pods', { error: error.message, criteria });
|
|
119
|
-
return [];
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Helper: Executes kubectl command with error handling.
|
|
125
|
-
* @method _executeKubectl
|
|
126
|
-
* @memberof UnderpostDB
|
|
127
|
-
* @param {string} command - kubectl command to execute.
|
|
128
|
-
* @param {Object} [options={}] - Execution options.
|
|
129
|
-
* @param {string} [options.context=''] - Command context for logging.
|
|
130
|
-
* @return {string|null} Command output or null on error.
|
|
131
|
-
*/
|
|
132
|
-
_executeKubectl(command, options = {}) {
|
|
133
|
-
const { context = '' } = options;
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
logger.info(`Executing kubectl command`, { command, context });
|
|
137
|
-
return shellExec(command, { stdout: true });
|
|
138
|
-
} catch (error) {
|
|
139
|
-
logger.error(`kubectl command failed`, { command, error: error.message, context });
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Helper: Copies file to pod.
|
|
146
|
-
* @method _copyToPod
|
|
147
|
-
* @memberof UnderpostDB
|
|
148
|
-
* @param {Object} params - Copy parameters.
|
|
149
|
-
* @param {string} params.sourcePath - Source file path.
|
|
150
|
-
* @param {string} params.podName - Target pod name.
|
|
151
|
-
* @param {string} params.namespace - Pod namespace.
|
|
152
|
-
* @param {string} params.destPath - Destination path in pod.
|
|
153
|
-
* @return {boolean} Success status.
|
|
154
|
-
*/
|
|
155
|
-
_copyToPod({ sourcePath, podName, namespace, destPath }) {
|
|
156
|
-
try {
|
|
157
|
-
const command = `sudo kubectl cp ${sourcePath} ${namespace}/${podName}:${destPath}`;
|
|
158
|
-
Underpost.db._executeKubectl(command, { context: `copy to pod ${podName}` });
|
|
159
|
-
return true;
|
|
160
|
-
} catch (error) {
|
|
161
|
-
logger.error('Failed to copy file to pod', { sourcePath, podName, destPath, error: error.message });
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Helper: Copies file from pod.
|
|
168
|
-
* @method _copyFromPod
|
|
169
|
-
* @memberof UnderpostDB
|
|
170
|
-
* @param {Object} params - Copy parameters.
|
|
171
|
-
* @param {string} params.podName - Source pod name.
|
|
172
|
-
* @param {string} params.namespace - Pod namespace.
|
|
173
|
-
* @param {string} params.sourcePath - Source path in pod.
|
|
174
|
-
* @param {string} params.destPath - Destination file path.
|
|
175
|
-
* @return {boolean} Success status.
|
|
176
|
-
*/
|
|
177
|
-
_copyFromPod({ podName, namespace, sourcePath, destPath }) {
|
|
178
|
-
try {
|
|
179
|
-
const command = `sudo kubectl cp ${namespace}/${podName}:${sourcePath} ${destPath}`;
|
|
180
|
-
Underpost.db._executeKubectl(command, { context: `copy from pod ${podName}` });
|
|
181
|
-
return true;
|
|
182
|
-
} catch (error) {
|
|
183
|
-
logger.error('Failed to copy file from pod', { podName, sourcePath, destPath, error: error.message });
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Helper: Executes command in pod.
|
|
190
|
-
* @method _execInPod
|
|
191
|
-
* @memberof UnderpostDB
|
|
192
|
-
* @param {Object} params - Execution parameters.
|
|
193
|
-
* @param {string} params.podName - Pod name.
|
|
194
|
-
* @param {string} params.namespace - Pod namespace.
|
|
195
|
-
* @param {string} params.command - Command to execute.
|
|
196
|
-
* @return {string|null} Command output or null.
|
|
197
|
-
*/
|
|
198
|
-
_execInPod({ podName, namespace, command }) {
|
|
199
|
-
try {
|
|
200
|
-
const kubectlCmd = `sudo kubectl exec -n ${namespace} -i ${podName} -- sh -c "${command}"`;
|
|
201
|
-
return Underpost.db._executeKubectl(kubectlCmd, { context: `exec in pod ${podName}` });
|
|
202
|
-
} catch (error) {
|
|
203
|
-
logger.error('Failed to execute command in pod', { podName, command, error: error.message });
|
|
204
|
-
throw error;
|
|
205
|
-
}
|
|
206
|
-
},
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Helper: Manages Git repository for backups.
|
|
210
|
-
* @method _manageGitRepo
|
|
211
|
-
* @memberof UnderpostDB
|
|
212
|
-
* @param {Object} params - Git parameters.
|
|
213
|
-
* @param {string} params.repoName - Repository name.
|
|
214
|
-
* @param {string} params.operation - Operation (clone, pull, commit, push).
|
|
215
|
-
* @param {string} [params.message=''] - Commit message.
|
|
216
|
-
* @param {boolean} [params.forceClone=false] - Force remove and re-clone repository.
|
|
217
|
-
* @return {boolean} Success status.
|
|
218
|
-
*/
|
|
219
|
-
_manageGitRepo({ repoName, operation, message = '', forceClone = false }) {
|
|
220
|
-
try {
|
|
221
|
-
const username = process.env.GITHUB_USERNAME;
|
|
222
|
-
if (!username) {
|
|
223
|
-
logger.error('GITHUB_USERNAME environment variable not set');
|
|
224
|
-
return false;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const repoPath = `../${repoName}`;
|
|
228
|
-
|
|
229
|
-
switch (operation) {
|
|
230
|
-
case 'clone':
|
|
231
|
-
if (forceClone && fs.existsSync(repoPath)) {
|
|
232
|
-
logger.info(`Force clone enabled, removing existing repository: ${repoName}`);
|
|
233
|
-
fs.removeSync(repoPath);
|
|
234
|
-
}
|
|
235
|
-
if (!fs.existsSync(repoPath)) {
|
|
236
|
-
shellExec(`cd .. && underpost clone ${username}/${repoName}`);
|
|
237
|
-
logger.info(`Cloned repository: ${repoName}`);
|
|
238
|
-
}
|
|
239
|
-
break;
|
|
240
|
-
|
|
241
|
-
case 'pull':
|
|
242
|
-
if (fs.existsSync(repoPath)) {
|
|
243
|
-
shellExec(`cd ${repoPath} && git checkout . && git clean -f -d`);
|
|
244
|
-
shellExec(`cd ${repoPath} && underpost pull . ${username}/${repoName}`, {
|
|
245
|
-
silent: true,
|
|
246
|
-
});
|
|
247
|
-
logger.info(`Pulled repository: ${repoName}`);
|
|
248
|
-
}
|
|
249
|
-
break;
|
|
250
|
-
|
|
251
|
-
case 'commit':
|
|
252
|
-
if (fs.existsSync(repoPath)) {
|
|
253
|
-
shellExec(`cd ${repoPath} && git add .`);
|
|
254
|
-
shellExec(`underpost cmt ${repoPath} backup '' '${message}'`);
|
|
255
|
-
logger.info(`Committed to repository: ${repoName}`, { message });
|
|
256
|
-
}
|
|
257
|
-
break;
|
|
258
|
-
|
|
259
|
-
case 'push':
|
|
260
|
-
if (fs.existsSync(repoPath)) {
|
|
261
|
-
shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName}`, { silent: true });
|
|
262
|
-
logger.info(`Pushed repository: ${repoName}`);
|
|
263
|
-
}
|
|
264
|
-
break;
|
|
265
|
-
|
|
266
|
-
default:
|
|
267
|
-
logger.warn(`Unknown git operation: ${operation}`);
|
|
268
|
-
return false;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return true;
|
|
272
|
-
} catch (error) {
|
|
273
|
-
logger.error(`Git operation failed`, { repoName, operation, error: error.message });
|
|
274
|
-
return false;
|
|
275
|
-
}
|
|
276
|
-
},
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Helper: Manages backup timestamps and cleanup.
|
|
280
|
-
* @method _manageBackupTimestamps
|
|
281
|
-
* @memberof UnderpostDB
|
|
282
|
-
* @param {string} backupPath - Backup directory path.
|
|
283
|
-
* @param {number} newTimestamp - New backup timestamp.
|
|
284
|
-
* @param {boolean} shouldCleanup - Whether to cleanup old backups.
|
|
285
|
-
* @return {Object} Backup info with current and removed timestamps.
|
|
286
|
-
*/
|
|
287
|
-
_manageBackupTimestamps(backupPath, newTimestamp, shouldCleanup) {
|
|
288
|
-
try {
|
|
289
|
-
if (!fs.existsSync(backupPath)) {
|
|
290
|
-
fs.mkdirSync(backupPath, { recursive: true });
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Delete empty folders
|
|
294
|
-
shellExec(`cd ${backupPath} && find . -type d -empty -delete`);
|
|
295
|
-
|
|
296
|
-
const times = fs.readdirSync(backupPath);
|
|
297
|
-
const validTimes = times.map((t) => parseInt(t)).filter((t) => !isNaN(t));
|
|
298
|
-
|
|
299
|
-
const currentBackupTimestamp = validTimes.length > 0 ? Math.max(...validTimes) : null;
|
|
300
|
-
const removeBackupTimestamp = validTimes.length > 0 ? Math.min(...validTimes) : null;
|
|
301
|
-
|
|
302
|
-
// Cleanup old backups if we have too many
|
|
303
|
-
if (shouldCleanup && validTimes.length >= MAX_BACKUP_RETENTION && removeBackupTimestamp) {
|
|
304
|
-
const removeDir = `${backupPath}/${removeBackupTimestamp}`;
|
|
305
|
-
logger.info('Removing old backup', { path: removeDir });
|
|
306
|
-
fs.removeSync(removeDir);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Create new backup directory
|
|
310
|
-
if (shouldCleanup) {
|
|
311
|
-
const newBackupDir = `${backupPath}/${newTimestamp}`;
|
|
312
|
-
logger.info('Creating new backup directory', { path: newBackupDir });
|
|
313
|
-
fs.mkdirSync(newBackupDir, { recursive: true });
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
current: currentBackupTimestamp,
|
|
318
|
-
removed: removeBackupTimestamp,
|
|
319
|
-
count: validTimes.length,
|
|
320
|
-
};
|
|
321
|
-
} catch (error) {
|
|
322
|
-
logger.error('Error managing backup timestamps', { backupPath, error: error.message });
|
|
323
|
-
return { current: null, removed: null, count: 0 };
|
|
324
|
-
}
|
|
95
|
+
_getLatestBackupTimestamp(backupDir) {
|
|
96
|
+
if (!fs.existsSync(backupDir)) return null;
|
|
97
|
+
const entries = fs.readdirSync(backupDir).filter((e) => /^\d+$/.test(e));
|
|
98
|
+
if (entries.length === 0) return null;
|
|
99
|
+
return entries.sort((a, b) => parseInt(b) - parseInt(a))[0];
|
|
325
100
|
},
|
|
326
101
|
|
|
327
102
|
/**
|
|
@@ -344,8 +119,20 @@ class UnderpostDB {
|
|
|
344
119
|
|
|
345
120
|
logger.info('Importing MariaDB database', { podName, dbName });
|
|
346
121
|
|
|
122
|
+
// Always ensure the database exists first — required for WP even when no backup is available
|
|
123
|
+
Underpost.kubectl.run(
|
|
124
|
+
`kubectl exec -n ${namespace} -i ${podName} -- mariadb -p${password} -e 'CREATE DATABASE IF NOT EXISTS ${dbName};'`,
|
|
125
|
+
{ context: `create database ${dbName}` },
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// If no SQL file is available, the empty database is enough — return early
|
|
129
|
+
if (!sqlPath || !fs.existsSync(sqlPath)) {
|
|
130
|
+
logger.warn('No SQL backup file found — empty database ensured', { podName, dbName, sqlPath });
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
347
134
|
// Remove existing SQL file in container
|
|
348
|
-
Underpost.
|
|
135
|
+
Underpost.kubectl.exec({
|
|
349
136
|
podName,
|
|
350
137
|
namespace,
|
|
351
138
|
command: `rm -rf ${containerSqlPath}`,
|
|
@@ -353,7 +140,7 @@ class UnderpostDB {
|
|
|
353
140
|
|
|
354
141
|
// Copy SQL file to pod
|
|
355
142
|
if (
|
|
356
|
-
!Underpost.
|
|
143
|
+
!Underpost.kubectl.cpTo({
|
|
357
144
|
sourcePath: sqlPath,
|
|
358
145
|
podName,
|
|
359
146
|
namespace,
|
|
@@ -363,15 +150,9 @@ class UnderpostDB {
|
|
|
363
150
|
return false;
|
|
364
151
|
}
|
|
365
152
|
|
|
366
|
-
// Create database if it doesn't exist
|
|
367
|
-
Underpost.db._executeKubectl(
|
|
368
|
-
`kubectl exec -n ${namespace} -i ${podName} -- mariadb -p${password} -e 'CREATE DATABASE IF NOT EXISTS ${dbName};'`,
|
|
369
|
-
{ context: `create database ${dbName}` },
|
|
370
|
-
);
|
|
371
|
-
|
|
372
153
|
// Import SQL file
|
|
373
154
|
const importCmd = `mariadb -u ${user} -p${password} ${dbName} < ${containerSqlPath}`;
|
|
374
|
-
Underpost.
|
|
155
|
+
Underpost.kubectl.exec({ podName, namespace, command: importCmd });
|
|
375
156
|
|
|
376
157
|
logger.info('Successfully imported MariaDB database', { podName, dbName });
|
|
377
158
|
return true;
|
|
@@ -402,7 +183,7 @@ class UnderpostDB {
|
|
|
402
183
|
logger.info('Exporting MariaDB database', { podName, dbName });
|
|
403
184
|
|
|
404
185
|
// Remove existing SQL file in container
|
|
405
|
-
Underpost.
|
|
186
|
+
Underpost.kubectl.exec({
|
|
406
187
|
podName,
|
|
407
188
|
namespace,
|
|
408
189
|
command: `rm -rf ${containerSqlPath}`,
|
|
@@ -410,11 +191,11 @@ class UnderpostDB {
|
|
|
410
191
|
|
|
411
192
|
// Dump database
|
|
412
193
|
const dumpCmd = `mariadb-dump --user=${user} --password=${password} --lock-tables ${dbName} > ${containerSqlPath}`;
|
|
413
|
-
Underpost.
|
|
194
|
+
Underpost.kubectl.exec({ podName, namespace, command: dumpCmd });
|
|
414
195
|
|
|
415
196
|
// Copy SQL file from pod
|
|
416
197
|
if (
|
|
417
|
-
!Underpost.
|
|
198
|
+
!Underpost.kubectl.cpFrom({
|
|
418
199
|
podName,
|
|
419
200
|
namespace,
|
|
420
201
|
sourcePath: containerSqlPath,
|
|
@@ -457,8 +238,18 @@ class UnderpostDB {
|
|
|
457
238
|
|
|
458
239
|
logger.info('Importing MongoDB database', { podName, dbName });
|
|
459
240
|
|
|
241
|
+
// If no BSON directory is available, skip — MongoDB creates the DB on first write
|
|
242
|
+
if (!bsonPath || !fs.existsSync(bsonPath)) {
|
|
243
|
+
logger.warn('No BSON backup directory found — database will be created on first write', {
|
|
244
|
+
podName,
|
|
245
|
+
dbName,
|
|
246
|
+
bsonPath,
|
|
247
|
+
});
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
460
251
|
// Remove existing BSON directory in container
|
|
461
|
-
Underpost.
|
|
252
|
+
Underpost.kubectl.exec({
|
|
462
253
|
podName,
|
|
463
254
|
namespace,
|
|
464
255
|
command: `rm -rf ${containerBsonPath}`,
|
|
@@ -466,7 +257,7 @@ class UnderpostDB {
|
|
|
466
257
|
|
|
467
258
|
// Copy BSON directory to pod
|
|
468
259
|
if (
|
|
469
|
-
!Underpost.
|
|
260
|
+
!Underpost.kubectl.cpTo({
|
|
470
261
|
sourcePath: bsonPath,
|
|
471
262
|
podName,
|
|
472
263
|
namespace,
|
|
@@ -480,7 +271,7 @@ class UnderpostDB {
|
|
|
480
271
|
const restoreCmd = `mongorestore -d ${dbName} ${containerBsonPath}${drop ? ' --drop' : ''}${
|
|
481
272
|
preserveUUID ? ' --preserveUUID' : ''
|
|
482
273
|
}`;
|
|
483
|
-
Underpost.
|
|
274
|
+
Underpost.kubectl.exec({ podName, namespace, command: restoreCmd });
|
|
484
275
|
|
|
485
276
|
logger.info('Successfully imported MongoDB database', { podName, dbName });
|
|
486
277
|
return true;
|
|
@@ -510,7 +301,7 @@ class UnderpostDB {
|
|
|
510
301
|
logger.info('Exporting MongoDB database', { podName, dbName, collections });
|
|
511
302
|
|
|
512
303
|
// Remove existing BSON directory in container
|
|
513
|
-
Underpost.
|
|
304
|
+
Underpost.kubectl.exec({
|
|
514
305
|
podName,
|
|
515
306
|
namespace,
|
|
516
307
|
command: `rm -rf ${containerBsonPath}`,
|
|
@@ -521,16 +312,16 @@ class UnderpostDB {
|
|
|
521
312
|
const collectionList = collections.split(',').map((c) => c.trim());
|
|
522
313
|
for (const collection of collectionList) {
|
|
523
314
|
const dumpCmd = `mongodump -d ${dbName} --collection ${collection} -o /`;
|
|
524
|
-
Underpost.
|
|
315
|
+
Underpost.kubectl.exec({ podName, namespace, command: dumpCmd });
|
|
525
316
|
}
|
|
526
317
|
} else {
|
|
527
318
|
const dumpCmd = `mongodump -d ${dbName} -o /`;
|
|
528
|
-
Underpost.
|
|
319
|
+
Underpost.kubectl.exec({ podName, namespace, command: dumpCmd });
|
|
529
320
|
}
|
|
530
321
|
|
|
531
322
|
// Copy BSON directory from pod
|
|
532
323
|
if (
|
|
533
|
-
!Underpost.
|
|
324
|
+
!Underpost.kubectl.cpFrom({
|
|
534
325
|
podName,
|
|
535
326
|
namespace,
|
|
536
327
|
sourcePath: containerBsonPath,
|
|
@@ -624,7 +415,7 @@ class UnderpostDB {
|
|
|
624
415
|
logger.info('Getting MariaDB table statistics', { podName, dbName });
|
|
625
416
|
|
|
626
417
|
const command = `sudo kubectl exec -n ${namespace} -i ${podName} -- mariadb -u ${user} -p${password} ${dbName} -e "SELECT TABLE_NAME as 'table', TABLE_ROWS as 'count' FROM information_schema.TABLES WHERE TABLE_SCHEMA = '${dbName}' ORDER BY TABLE_NAME;" --skip-column-names --batch`;
|
|
627
|
-
const output = shellExec(command, { stdout: true, silent: true });
|
|
418
|
+
const output = shellExec(command, { stdout: true, silent: true, disableLog: true });
|
|
628
419
|
|
|
629
420
|
if (!output || output.trim() === '') {
|
|
630
421
|
logger.warn('No tables found or empty output');
|
|
@@ -758,6 +549,7 @@ class UnderpostDB {
|
|
|
758
549
|
* @param {boolean} [options.k3s=false] - k3s cluster flag.
|
|
759
550
|
* @param {boolean} [options.kubeadm=false] - kubeadm cluster flag.
|
|
760
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.
|
|
761
553
|
* @return {Promise<void>} Resolves when operation is complete.
|
|
762
554
|
*/
|
|
763
555
|
async callback(
|
|
@@ -786,350 +578,375 @@ class UnderpostDB {
|
|
|
786
578
|
k3s: false,
|
|
787
579
|
kubeadm: false,
|
|
788
580
|
kind: false,
|
|
581
|
+
repoBackup: false,
|
|
789
582
|
},
|
|
790
583
|
) {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
+
}
|
|
806
609
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
+
}
|
|
620
|
+
|
|
621
|
+
logger.info('Starting database operation', {
|
|
622
|
+
deployList,
|
|
623
|
+
namespace,
|
|
624
|
+
import: options.import,
|
|
625
|
+
export: options.export,
|
|
626
|
+
});
|
|
813
627
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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;
|
|
821
637
|
}
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
638
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
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();
|
|
829
643
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
644
|
+
for (const _deployId of deployList.split(',')) {
|
|
645
|
+
const deployId = _deployId.trim();
|
|
646
|
+
if (!deployId) continue;
|
|
833
647
|
|
|
834
|
-
|
|
648
|
+
logger.info('Processing deployment', { deployId });
|
|
835
649
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
650
|
+
/** @type {Object.<string, Object.<string, DatabaseConfig>>} */
|
|
651
|
+
const dbs = {};
|
|
652
|
+
const repoName = `engine-${deployId.includes('dd-') ? deployId.split('dd-')[1] : deployId}-cron-backups`;
|
|
839
653
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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
|
+
}
|
|
846
660
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
+
}
|
|
865
680
|
}
|
|
866
681
|
}
|
|
867
682
|
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// Handle Git operations - execute only once per repository
|
|
871
|
-
if (!processedRepos.has(repoName)) {
|
|
872
|
-
logger.info('Processing Git operations for repository', { repoName, deployId });
|
|
873
|
-
if (options.git === true) {
|
|
874
|
-
Underpost.db._manageGitRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
|
|
875
|
-
Underpost.db._manageGitRepo({ repoName, operation: 'pull' });
|
|
876
|
-
}
|
|
877
683
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
Underpost.
|
|
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) {
|
|
688
|
+
Underpost.repo.manageBackupRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
|
|
689
|
+
Underpost.repo.manageBackupRepo({ repoName, operation: 'pull' });
|
|
883
690
|
}
|
|
884
691
|
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
shellExec(`cd ${repoPath} && underpost cmt . reset ${nCommits}`);
|
|
892
|
-
shellExec(`cd ${repoPath} && git reset`);
|
|
893
|
-
shellExec(`cd ${repoPath} && git checkout .`);
|
|
894
|
-
shellExec(`cd ${repoPath} && git clean -f -d`);
|
|
895
|
-
shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName} -f`);
|
|
896
|
-
} else {
|
|
897
|
-
if (!username) logger.error('GITHUB_USERNAME environment variable not set');
|
|
898
|
-
logger.warn('Repository not found for macro rollback', { repoPath });
|
|
899
|
-
}
|
|
900
|
-
}
|
|
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
|
+
}
|
|
901
698
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
+
}
|
|
714
|
+
}
|
|
907
715
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
+
}
|
|
912
721
|
|
|
913
|
-
|
|
914
|
-
|
|
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];
|
|
915
726
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
logger.info('Skipping already processed host/path', { dbName, host, path, deployId });
|
|
919
|
-
continue;
|
|
920
|
-
}
|
|
727
|
+
// Create unique identifier for host+path combination
|
|
728
|
+
const hostPathKey = `${deployId}:${host}:${path}`;
|
|
921
729
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
.map((h) => h.trim())
|
|
928
|
-
.includes(host)) ||
|
|
929
|
-
(options.paths &&
|
|
930
|
-
!options.paths
|
|
931
|
-
.split(',')
|
|
932
|
-
.map((p) => p.trim())
|
|
933
|
-
.includes(path))
|
|
934
|
-
) {
|
|
935
|
-
logger.info('Skipping database due to host/path filter', { dbName, host, path });
|
|
936
|
-
continue;
|
|
937
|
-
}
|
|
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
|
+
}
|
|
938
735
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
+
}
|
|
943
752
|
|
|
944
|
-
|
|
753
|
+
if (!hostFolder) {
|
|
754
|
+
logger.warn('No hostFolder defined for database', { dbName, provider });
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
945
757
|
|
|
946
|
-
|
|
947
|
-
const backupInfo = Underpost.db._manageBackupTimestamps(
|
|
948
|
-
backUpPath,
|
|
949
|
-
newBackupTimestamp,
|
|
950
|
-
options.export === true,
|
|
951
|
-
);
|
|
758
|
+
logger.info('Processing database', { hostFolder, provider, dbName, deployId });
|
|
952
759
|
|
|
953
|
-
|
|
760
|
+
const latestBackupTimestamp = Underpost.db._getLatestBackupTimestamp(`../${repoName}/${hostFolder}`);
|
|
954
761
|
|
|
955
|
-
|
|
956
|
-
const sqlContainerPath = `/home/${dbName}.sql`;
|
|
957
|
-
const fromPartsPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}-parths.json`;
|
|
958
|
-
const toSqlPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}.sql`;
|
|
959
|
-
const toNewSqlPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}.sql`;
|
|
960
|
-
const toBsonPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}`;
|
|
961
|
-
const toNewBsonPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}`;
|
|
762
|
+
dbs[provider][dbName].currentBackupTimestamp = latestBackupTimestamp;
|
|
962
763
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
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}`;
|
|
971
771
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
+
}
|
|
979
780
|
|
|
980
|
-
|
|
781
|
+
// Get target pods based on provider and options
|
|
782
|
+
let targetPods = [];
|
|
783
|
+
const podCriteria = {
|
|
784
|
+
podNames: options.podName,
|
|
785
|
+
namespace,
|
|
786
|
+
deployId: provider === 'mariadb' ? 'mariadb' : 'mongo',
|
|
787
|
+
};
|
|
981
788
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
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
|
+
}
|
|
988
801
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
802
|
+
if (targetPods.length === 0) {
|
|
803
|
+
logger.warn('No pods found matching criteria', { provider, criteria: podCriteria });
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
993
806
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
+
}
|
|
1010
827
|
} else {
|
|
1011
|
-
logger.warn('
|
|
828
|
+
logger.warn('Could not detect primary pod, using first pod');
|
|
1012
829
|
podsToProcess = [targetPods[0]];
|
|
1013
830
|
}
|
|
1014
|
-
} else {
|
|
1015
|
-
logger.warn('Could not detect primary pod, using first pod');
|
|
1016
|
-
podsToProcess = [targetPods[0]];
|
|
1017
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]];
|
|
1018
835
|
}
|
|
1019
|
-
} else {
|
|
1020
|
-
// For MariaDB or when allPods is true, limit to first pod unless allPods is true
|
|
1021
|
-
podsToProcess = options.allPods === true ? targetPods : [targetPods[0]];
|
|
1022
|
-
}
|
|
1023
836
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
837
|
+
logger.info(`Processing ${podsToProcess.length} pod(s) for ${provider}`, {
|
|
838
|
+
dbName,
|
|
839
|
+
pods: podsToProcess.map((p) => p.NAME),
|
|
840
|
+
});
|
|
1028
841
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
+
}
|
|
1045
859
|
}
|
|
1046
|
-
}
|
|
1047
860
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
861
|
+
if (options.import === true) {
|
|
862
|
+
Underpost.db._importMariaDB({
|
|
863
|
+
pod,
|
|
864
|
+
namespace,
|
|
865
|
+
dbName,
|
|
866
|
+
user,
|
|
867
|
+
password,
|
|
868
|
+
sqlPath: toSqlPath,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
1058
871
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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;
|
|
1069
884
|
}
|
|
1070
|
-
break;
|
|
1071
|
-
}
|
|
1072
885
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
+
}
|
|
1082
896
|
}
|
|
1083
|
-
}
|
|
1084
897
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
+
}
|
|
1096
909
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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;
|
|
1106
921
|
}
|
|
1107
|
-
break;
|
|
1108
|
-
}
|
|
1109
922
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
923
|
+
default:
|
|
924
|
+
logger.warn('Unsupported database provider', { provider });
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
1113
927
|
}
|
|
928
|
+
|
|
929
|
+
// Mark this host+path combination as processed
|
|
930
|
+
processedHostPaths.add(hostPathKey);
|
|
1114
931
|
}
|
|
932
|
+
}
|
|
1115
933
|
|
|
1116
|
-
|
|
1117
|
-
|
|
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`);
|
|
1118
942
|
}
|
|
1119
943
|
}
|
|
1120
944
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
).toLocaleTimeString()}`;
|
|
1126
|
-
Underpost.db._manageGitRepo({ repoName, operation: 'commit', message: commitMessage });
|
|
1127
|
-
Underpost.db._manageGitRepo({ repoName, operation: 'push' });
|
|
1128
|
-
processedRepos.add(`${repoName}-committed`);
|
|
1129
|
-
}
|
|
945
|
+
logger.info('Database operation completed successfully');
|
|
946
|
+
} catch (error) {
|
|
947
|
+
logger.error('Database operation failed', { error: error.message });
|
|
948
|
+
throw error;
|
|
1130
949
|
}
|
|
1131
|
-
|
|
1132
|
-
logger.info('Database operation completed successfully');
|
|
1133
950
|
},
|
|
1134
951
|
|
|
1135
952
|
/**
|
|
@@ -1141,6 +958,8 @@ class UnderpostDB {
|
|
|
1141
958
|
* @param {string} [deployId=process.env.DEFAULT_DEPLOY_ID] - The deployment ID.
|
|
1142
959
|
* @param {string} [host=process.env.DEFAULT_DEPLOY_HOST] - The host identifier.
|
|
1143
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.
|
|
1144
963
|
* @return {Promise<void>} Resolves when metadata creation is complete.
|
|
1145
964
|
* @throws {Error} If database configuration is invalid or connection fails.
|
|
1146
965
|
*/
|
|
@@ -1148,160 +967,181 @@ class UnderpostDB {
|
|
|
1148
967
|
deployId = process.env.DEFAULT_DEPLOY_ID,
|
|
1149
968
|
host = process.env.DEFAULT_DEPLOY_HOST,
|
|
1150
969
|
path = process.env.DEFAULT_DEPLOY_PATH,
|
|
970
|
+
options = { dev: false },
|
|
1151
971
|
) {
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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;
|
|
1155
977
|
|
|
1156
|
-
|
|
978
|
+
logger.info('Creating cluster metadata', { deployId, host, path });
|
|
1157
979
|
|
|
1158
|
-
|
|
1159
|
-
|
|
980
|
+
const env = 'production';
|
|
981
|
+
const deployListPath = './engine-private/deploy/dd.router';
|
|
1160
982
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
+
}
|
|
1165
987
|
|
|
1166
|
-
|
|
988
|
+
const deployList = fs.readFileSync(deployListPath, 'utf8').split(',');
|
|
1167
989
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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
|
+
}
|
|
1173
995
|
|
|
1174
|
-
|
|
996
|
+
const { db } = loadConfServerJson(confServerPath, { resolve: true })[host][path];
|
|
1175
997
|
|
|
1176
|
-
|
|
1177
|
-
|
|
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);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1178
1013
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1014
|
+
try {
|
|
1015
|
+
/** @type {import('../api/instance/instance.model.js').InstanceModel} */
|
|
1016
|
+
const Instance = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Instance;
|
|
1181
1017
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1018
|
+
await Instance.deleteMany();
|
|
1019
|
+
logger.info('Cleared existing instance metadata');
|
|
1184
1020
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1021
|
+
for (const _deployId of deployList) {
|
|
1022
|
+
const deployId = _deployId.trim();
|
|
1023
|
+
if (!deployId) continue;
|
|
1188
1024
|
|
|
1189
|
-
|
|
1025
|
+
logger.info('Processing deployment for metadata', { deployId });
|
|
1190
1026
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
+
}
|
|
1196
1032
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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);
|
|
1200
1036
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1037
|
+
for (const host of Object.keys(confServer)) {
|
|
1038
|
+
for (const { path, port } of pathPortAssignmentData[host]) {
|
|
1039
|
+
if (!confServer[host][path]) continue;
|
|
1204
1040
|
|
|
1205
|
-
|
|
1041
|
+
const { client, runtime, apis, peer } = confServer[host][path];
|
|
1206
1042
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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
|
+
}
|
|
1218
1058
|
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
+
}
|
|
1221
1072
|
}
|
|
1073
|
+
}
|
|
1222
1074
|
|
|
1223
|
-
|
|
1224
|
-
|
|
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;
|
|
1225
1082
|
const body = {
|
|
1226
1083
|
deployId,
|
|
1227
1084
|
host,
|
|
1228
|
-
path
|
|
1229
|
-
port:
|
|
1230
|
-
|
|
1085
|
+
path,
|
|
1086
|
+
port: fromPort,
|
|
1087
|
+
client: id,
|
|
1088
|
+
runtime,
|
|
1231
1089
|
};
|
|
1232
|
-
|
|
1233
|
-
logger.info('Saving peer instance metadata', body);
|
|
1090
|
+
logger.info('Saving additional instance metadata', body);
|
|
1234
1091
|
await new Instance(body).save();
|
|
1235
1092
|
}
|
|
1236
1093
|
}
|
|
1237
1094
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
if (fs.existsSync(confInstancesPath)) {
|
|
1242
|
-
const confInstances = JSON.parse(fs.readFileSync(confInstancesPath, 'utf8'));
|
|
1243
|
-
for (const instance of confInstances) {
|
|
1244
|
-
const { id, host, path, fromPort, metadata } = instance;
|
|
1245
|
-
const { runtime } = metadata;
|
|
1246
|
-
const body = {
|
|
1247
|
-
deployId,
|
|
1248
|
-
host,
|
|
1249
|
-
path,
|
|
1250
|
-
port: fromPort,
|
|
1251
|
-
client: id,
|
|
1252
|
-
runtime,
|
|
1253
|
-
};
|
|
1254
|
-
logger.info('Saving additional instance metadata', body);
|
|
1255
|
-
await new Instance(body).save();
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
logger.error('Failed to create instance metadata', { error: error.message });
|
|
1097
|
+
throw error;
|
|
1258
1098
|
}
|
|
1259
|
-
} catch (error) {
|
|
1260
|
-
logger.error('Failed to create instance metadata', { error: error.message, stack: error.stack });
|
|
1261
|
-
throw error;
|
|
1262
|
-
}
|
|
1263
1099
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
+
}
|
|
1270
1106
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1107
|
+
const cronDeployId = fs.readFileSync(cronDeployPath, 'utf8').trim();
|
|
1108
|
+
const confCronPath = `./engine-private/conf/${cronDeployId}/conf.cron.json`;
|
|
1273
1109
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1110
|
+
if (!fs.existsSync(confCronPath)) {
|
|
1111
|
+
logger.warn('Cron configuration not found', { path: confCronPath });
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1278
1114
|
|
|
1279
|
-
|
|
1115
|
+
const confCron = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
|
|
1280
1116
|
|
|
1281
|
-
|
|
1117
|
+
await DataBaseProvider.load({ apis: ['cron'], host, path, db });
|
|
1282
1118
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1119
|
+
/** @type {import('../api/cron/cron.model.js').CronModel} */
|
|
1120
|
+
const Cron = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Cron;
|
|
1285
1121
|
|
|
1286
|
-
|
|
1287
|
-
|
|
1122
|
+
await Cron.deleteMany();
|
|
1123
|
+
logger.info('Cleared existing cron metadata');
|
|
1288
1124
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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 });
|
|
1298
1137
|
}
|
|
1138
|
+
|
|
1139
|
+
await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
|
|
1140
|
+
logger.info('Cluster metadata creation completed');
|
|
1299
1141
|
} catch (error) {
|
|
1300
|
-
logger.error('
|
|
1142
|
+
logger.error('Cluster metadata creation failed', { error: error.message });
|
|
1143
|
+
throw error;
|
|
1301
1144
|
}
|
|
1302
|
-
|
|
1303
|
-
await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
|
|
1304
|
-
logger.info('Cluster metadata creation completed');
|
|
1305
1145
|
},
|
|
1306
1146
|
|
|
1307
1147
|
/**
|
|
@@ -1315,6 +1155,7 @@ class UnderpostDB {
|
|
|
1315
1155
|
* @param {string} [options.hosts=''] - Comma-separated list of hosts to filter.
|
|
1316
1156
|
* @param {string} [options.paths=''] - Comma-separated list of paths to filter.
|
|
1317
1157
|
* @param {boolean} [options.dryRun=false] - If true, only reports what would be deleted.
|
|
1158
|
+
* @param {boolean} [options.dev=false] - Development mode flag.
|
|
1318
1159
|
* @return {Promise<void>} Resolves when clean operation is complete.
|
|
1319
1160
|
*/
|
|
1320
1161
|
async cleanFsCollection(
|
|
@@ -1323,203 +1164,220 @@ class UnderpostDB {
|
|
|
1323
1164
|
hosts: '',
|
|
1324
1165
|
paths: '',
|
|
1325
1166
|
dryRun: false,
|
|
1167
|
+
dev: false,
|
|
1326
1168
|
},
|
|
1327
1169
|
) {
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
// Load file.ref.json to know which models reference File
|
|
1333
|
-
const fileRefPath = './src/api/file/file.ref.json';
|
|
1334
|
-
if (!fs.existsSync(fileRefPath)) {
|
|
1335
|
-
logger.error('file.ref.json not found', { path: fileRefPath });
|
|
1336
|
-
return;
|
|
1337
|
-
}
|
|
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');
|
|
1338
1174
|
|
|
1339
|
-
|
|
1340
|
-
logger.info('Loaded file reference configuration', { apis: fileRefData.length });
|
|
1175
|
+
logger.info('Starting File collection cleanup', { deployList, options });
|
|
1341
1176
|
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
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
|
+
}
|
|
1345
1183
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1184
|
+
const fileRefData = JSON.parse(fs.readFileSync(fileRefPath, 'utf8'));
|
|
1185
|
+
logger.info('Loaded file reference configuration', { apis: fileRefData.length });
|
|
1348
1186
|
|
|
1349
|
-
|
|
1350
|
-
const
|
|
1351
|
-
|
|
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()) : [];
|
|
1352
1190
|
|
|
1353
|
-
|
|
1191
|
+
// Track all connections to close them at the end
|
|
1192
|
+
const connectionsToClose = [];
|
|
1354
1193
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
logger.error('Configuration file not found', { path: confServerPath });
|
|
1359
|
-
continue;
|
|
1360
|
-
}
|
|
1194
|
+
for (const _deployId of deployList.split(',')) {
|
|
1195
|
+
const deployId = _deployId.trim();
|
|
1196
|
+
if (!deployId) continue;
|
|
1361
1197
|
|
|
1362
|
-
|
|
1198
|
+
logger.info('Processing deployment for File cleanup', { deployId });
|
|
1363
1199
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
if (
|
|
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
|
+
}
|
|
1367
1206
|
|
|
1368
|
-
|
|
1369
|
-
if (filterPaths.length > 0 && !filterPaths.includes(path)) continue;
|
|
1207
|
+
const confServer = loadConfServerJson(confServerPath, { resolve: true });
|
|
1370
1208
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1209
|
+
// Process each host+path combination
|
|
1210
|
+
for (const host of Object.keys(confServer)) {
|
|
1211
|
+
if (filterHosts.length > 0 && !filterHosts.includes(host)) continue;
|
|
1373
1212
|
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
logger.info('Skipping - no file api in configuration', { host, path });
|
|
1377
|
-
continue;
|
|
1378
|
-
}
|
|
1213
|
+
for (const path of Object.keys(confServer[host])) {
|
|
1214
|
+
if (filterPaths.length > 0 && !filterPaths.includes(path)) continue;
|
|
1379
1215
|
|
|
1380
|
-
|
|
1216
|
+
const { db, apis } = confServer[host][path];
|
|
1217
|
+
if (!db || !apis) continue;
|
|
1381
1218
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
if (!dbProvider || !dbProvider.models) {
|
|
1386
|
-
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 });
|
|
1387
1222
|
continue;
|
|
1388
1223
|
}
|
|
1389
1224
|
|
|
1390
|
-
|
|
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
|
+
}
|
|
1391
1244
|
|
|
1392
|
-
|
|
1393
|
-
connectionsToClose.push({ host, path, dbProvider });
|
|
1245
|
+
const { models } = dbProvider;
|
|
1394
1246
|
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
logger.warn('File model not loaded', { host, path });
|
|
1398
|
-
continue;
|
|
1399
|
-
}
|
|
1247
|
+
// Track this connection for cleanup
|
|
1248
|
+
connectionsToClose.push({ host, path, dbProvider });
|
|
1400
1249
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1250
|
+
// Check if File model exists
|
|
1251
|
+
if (!models.File) {
|
|
1252
|
+
logger.warn('File model not loaded', { host, path });
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1404
1255
|
|
|
1405
|
-
|
|
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 });
|
|
1406
1259
|
|
|
1407
|
-
|
|
1408
|
-
const referencedFileIds = new Set();
|
|
1260
|
+
if (allFiles.length === 0) continue;
|
|
1409
1261
|
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
const { api, model: modelFields } = refConfig;
|
|
1262
|
+
// Track which File IDs are referenced
|
|
1263
|
+
const referencedFileIds = new Set();
|
|
1413
1264
|
|
|
1414
|
-
// Check
|
|
1415
|
-
const
|
|
1416
|
-
|
|
1417
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1418
|
-
.join('');
|
|
1419
|
-
const Model = models[modelName];
|
|
1265
|
+
// Check each API from file.ref.json
|
|
1266
|
+
for (const refConfig of fileRefData) {
|
|
1267
|
+
const { api, model: modelFields } = refConfig;
|
|
1420
1268
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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];
|
|
1425
1275
|
|
|
1426
|
-
|
|
1276
|
+
if (!Model) {
|
|
1277
|
+
logger.debug('Model not loaded in current context', { api, modelName, host, path });
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1427
1280
|
|
|
1428
|
-
|
|
1429
|
-
const checkFieldReferences = async (fieldPath, fieldConfig) => {
|
|
1430
|
-
for (const [fieldName, fieldValue] of Object.entries(fieldConfig)) {
|
|
1431
|
-
const currentPath = fieldPath ? `${fieldPath}.${fieldName}` : fieldName;
|
|
1281
|
+
logger.info('Checking references in model', { api, modelName });
|
|
1432
1282
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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;
|
|
1437
1287
|
|
|
1438
|
-
|
|
1288
|
+
if (fieldValue === true) {
|
|
1289
|
+
// This is a File reference field
|
|
1290
|
+
const query = {};
|
|
1291
|
+
query[currentPath] = { $exists: true, $ne: null };
|
|
1439
1292
|
|
|
1440
|
-
|
|
1441
|
-
// Navigate to the nested field
|
|
1442
|
-
const parts = currentPath.split('.');
|
|
1443
|
-
let value = doc;
|
|
1444
|
-
for (const part of parts) {
|
|
1445
|
-
value = value?.[part];
|
|
1446
|
-
}
|
|
1293
|
+
const docs = await Model.find(query, currentPath).lean();
|
|
1447
1294
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
+
}
|
|
1302
|
+
|
|
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
|
+
}
|
|
1453
1309
|
}
|
|
1454
1310
|
}
|
|
1455
|
-
}
|
|
1456
1311
|
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
+
}
|
|
1465
1321
|
}
|
|
1466
|
-
}
|
|
1467
|
-
};
|
|
1468
|
-
|
|
1469
|
-
await checkFieldReferences('', modelFields);
|
|
1470
|
-
}
|
|
1322
|
+
};
|
|
1471
1323
|
|
|
1472
|
-
|
|
1324
|
+
await checkFieldReferences('', modelFields);
|
|
1325
|
+
}
|
|
1473
1326
|
|
|
1474
|
-
|
|
1475
|
-
const orphanedFiles = allFiles.filter((file) => !referencedFileIds.has(file._id.toString()));
|
|
1327
|
+
logger.info('Total referenced File IDs', { count: referencedFileIds.size, host, path });
|
|
1476
1328
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
} else {
|
|
1480
|
-
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()));
|
|
1481
1331
|
|
|
1482
|
-
if (
|
|
1483
|
-
logger.info('
|
|
1484
|
-
count: orphanedFiles.length,
|
|
1485
|
-
ids: orphanedFiles.map((f) => f._id.toString()),
|
|
1486
|
-
});
|
|
1332
|
+
if (orphanedFiles.length === 0) {
|
|
1333
|
+
logger.info('No orphaned files found', { host, path });
|
|
1487
1334
|
} else {
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
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
|
+
}
|
|
1495
1351
|
}
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
logger.error('Error processing host+path', {
|
|
1354
|
+
host,
|
|
1355
|
+
path,
|
|
1356
|
+
error: error.message,
|
|
1357
|
+
});
|
|
1496
1358
|
}
|
|
1497
|
-
} catch (error) {
|
|
1498
|
-
logger.error('Error processing host+path', {
|
|
1499
|
-
host,
|
|
1500
|
-
path,
|
|
1501
|
-
error: error.message,
|
|
1502
|
-
stack: error.stack,
|
|
1503
|
-
});
|
|
1504
1359
|
}
|
|
1505
1360
|
}
|
|
1506
1361
|
}
|
|
1507
|
-
}
|
|
1508
1362
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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 });
|
|
1516
1373
|
}
|
|
1517
|
-
} catch (error) {
|
|
1518
|
-
logger.error('Error closing connection', { host, path, error: error.message });
|
|
1519
1374
|
}
|
|
1520
|
-
}
|
|
1521
1375
|
|
|
1522
|
-
|
|
1376
|
+
logger.info('File collection cleanup completed');
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
logger.error('File collection cleanup failed', { error: error.message });
|
|
1379
|
+
throw error;
|
|
1380
|
+
}
|
|
1523
1381
|
},
|
|
1524
1382
|
|
|
1525
1383
|
/**
|
|
@@ -1538,6 +1396,7 @@ class UnderpostDB {
|
|
|
1538
1396
|
* @param {boolean} [options.export=false] - Export metadata to backup.
|
|
1539
1397
|
* @param {boolean} [options.instances=false] - Process instances collection.
|
|
1540
1398
|
* @param {boolean} [options.crons=false] - Process crons collection.
|
|
1399
|
+
* @param {boolean} [options.dev=false] - Development mode flag.
|
|
1541
1400
|
* @return {Promise<void>} Resolves when backup operation is complete.
|
|
1542
1401
|
*/
|
|
1543
1402
|
async clusterMetadataBackupCallback(
|
|
@@ -1551,69 +1410,76 @@ class UnderpostDB {
|
|
|
1551
1410
|
export: false,
|
|
1552
1411
|
instances: false,
|
|
1553
1412
|
crons: false,
|
|
1413
|
+
dev: false,
|
|
1554
1414
|
},
|
|
1555
1415
|
) {
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
logger.info('Generating cluster metadata');
|
|
1569
|
-
await Underpost.db.clusterMetadataFactory(deployId, host, path);
|
|
1570
|
-
}
|
|
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
|
+
});
|
|
1571
1428
|
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
fs.mkdirSync(outputPath, { recursive: true });
|
|
1429
|
+
if (options.generate === true) {
|
|
1430
|
+
logger.info('Generating cluster metadata');
|
|
1431
|
+
await Underpost.db.clusterMetadataFactory(deployId, host, path);
|
|
1576
1432
|
}
|
|
1577
|
-
const collection = 'instances';
|
|
1578
1433
|
|
|
1579
|
-
if (options.
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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';
|
|
1585
1440
|
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
}
|
|
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
|
+
}
|
|
1593
1447
|
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
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
|
+
}
|
|
1598
1454
|
}
|
|
1599
|
-
const collection = 'crons';
|
|
1600
1455
|
|
|
1601
|
-
if (options.
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
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
|
+
}
|
|
1607
1469
|
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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
|
+
}
|
|
1613
1476
|
}
|
|
1614
|
-
}
|
|
1615
1477
|
|
|
1616
|
-
|
|
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
|
+
}
|
|
1617
1483
|
},
|
|
1618
1484
|
};
|
|
1619
1485
|
}
|