cyberia 3.1.3 → 3.2.9
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.example +0 -2
- package/.github/workflows/engine-cyberia.cd.yml +10 -8
- package/.github/workflows/engine-cyberia.ci.yml +12 -29
- package/.github/workflows/ghpkg.ci.yml +4 -4
- package/.github/workflows/npmpkg.ci.yml +28 -11
- package/.github/workflows/publish.ci.yml +21 -2
- package/.github/workflows/pwa-microservices-template-page.cd.yml +4 -5
- package/.github/workflows/pwa-microservices-template-test.ci.yml +3 -3
- package/.github/workflows/release.cd.yml +14 -10
- package/CHANGELOG.md +783 -1
- package/CLI-HELP.md +95 -18
- package/Dockerfile +0 -2
- package/README.md +290 -220
- package/bin/build.js +24 -7
- package/bin/cyberia.js +2838 -252
- package/bin/deploy.js +747 -125
- package/bin/file.js +9 -0
- package/bin/index.js +2838 -252
- package/bin/vs.js +1 -1
- package/conf.js +99 -65
- package/deployment.yaml +18 -164
- package/hardhat/hardhat.config.js +13 -13
- package/hardhat/ignition/modules/ObjectLayerToken.js +1 -1
- package/hardhat/package-lock.json +2559 -5864
- package/hardhat/package.json +14 -23
- package/hardhat/scripts/deployObjectLayerToken.js +1 -1
- package/hardhat/test/ObjectLayerToken.js +4 -2
- 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/jsconfig.json +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +6 -5
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +6 -5
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +18 -164
- package/manifests/deployment/dd-cyberia-development/proxy.yaml +7 -79
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -6
- package/manifests/deployment/dd-test-development/deployment.yaml +112 -28
- package/manifests/deployment/dd-test-development/proxy.yaml +46 -1
- package/manifests/deployment/playwright/deployment.yaml +1 -1
- package/nodemon.json +1 -1
- package/package.json +39 -24
- package/proxy.yaml +7 -79
- package/scripts/k3s-node-setup.sh +2 -2
- package/scripts/nat-iptables.sh +103 -18
- package/scripts/rhel-grpc-setup.sh +56 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +58 -14
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +23 -14
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +148 -20
- package/src/api/core/core.controller.js +10 -10
- package/src/api/core/core.service.js +10 -10
- package/src/api/crypto/crypto.controller.js +8 -8
- package/src/api/crypto/crypto.service.js +8 -8
- package/src/api/cyberia-action/cyberia-action.controller.js +74 -0
- package/src/api/cyberia-action/cyberia-action.model.js +87 -0
- package/src/api/cyberia-action/cyberia-action.router.js +27 -0
- package/src/api/cyberia-action/cyberia-action.service.js +42 -0
- 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 +178 -0
- package/src/api/cyberia-instance/cyberia-instance.controller.js +92 -0
- package/src/api/cyberia-instance/cyberia-instance.model.js +87 -0
- package/src/api/cyberia-instance/cyberia-instance.router.js +63 -0
- package/src/api/cyberia-instance/cyberia-instance.service.js +156 -0
- package/src/api/cyberia-instance/cyberia-portal-connector.js +260 -0
- package/src/api/cyberia-instance/cyberia-world-generator.js +505 -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 +574 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +231 -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 +46 -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/cyberia-quest/cyberia-quest.controller.js +74 -0
- package/src/api/cyberia-quest/cyberia-quest.model.js +67 -0
- package/src/api/cyberia-quest/cyberia-quest.router.js +27 -0
- package/src/api/cyberia-quest/cyberia-quest.service.js +42 -0
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +74 -0
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +49 -0
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +27 -0
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +42 -0
- package/src/api/default/default.controller.js +10 -10
- package/src/api/default/default.service.js +10 -10
- package/src/api/document/document.controller.js +12 -12
- package/src/api/document/document.model.js +10 -16
- package/src/api/file/file.controller.js +8 -8
- package/src/api/file/file.model.js +10 -10
- package/src/api/file/file.ref.json +18 -0
- package/src/api/file/file.service.js +36 -36
- package/src/api/instance/instance.controller.js +10 -10
- package/src/api/instance/instance.model.js +4 -10
- package/src/api/instance/instance.service.js +10 -10
- package/src/api/ipfs/ipfs.controller.js +15 -36
- package/src/api/ipfs/ipfs.model.js +47 -47
- package/src/api/ipfs/ipfs.router.js +8 -13
- package/src/api/ipfs/ipfs.service.js +67 -129
- package/src/api/object-layer/object-layer.controller.js +12 -12
- package/src/api/object-layer/object-layer.model.js +4 -17
- package/src/api/object-layer/object-layer.router.js +30 -0
- package/src/api/object-layer/object-layer.service.js +126 -43
- package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +10 -10
- package/src/api/object-layer-render-frames/object-layer-render-frames.model.js +6 -16
- package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +18 -14
- package/src/api/test/test.controller.js +8 -8
- package/src/api/test/test.service.js +8 -8
- package/src/api/user/guest.service.js +99 -0
- package/src/api/user/user.controller.js +6 -6
- package/src/api/user/user.model.js +8 -13
- package/src/api/user/user.service.js +11 -27
- package/src/cli/cluster.js +68 -21
- package/src/cli/db.js +753 -825
- package/src/cli/deploy.js +215 -125
- package/src/cli/env.js +29 -0
- package/src/cli/fs.js +82 -8
- package/src/cli/image.js +43 -1
- package/src/cli/index.js +74 -3
- package/src/cli/kubectl.js +211 -0
- package/src/cli/release.js +340 -0
- package/src/cli/repository.js +475 -74
- package/src/cli/run.js +582 -43
- package/src/cli/secrets.js +73 -0
- package/src/cli/ssh.js +1 -1
- package/src/cli/static.js +43 -115
- package/src/cli/test.js +3 -3
- package/src/client/Cryptokoyn.index.js +18 -22
- package/src/client/CyberiaPortal.index.js +19 -24
- package/src/client/Default.index.js +21 -34
- package/src/client/Itemledger.index.js +20 -27
- package/src/client/Underpost.index.js +19 -24
- package/src/client/components/core/404.js +4 -4
- package/src/client/components/core/500.js +4 -4
- package/src/client/components/core/Account.js +73 -60
- package/src/client/components/core/AgGrid.js +23 -33
- package/src/client/components/core/Alert.js +12 -13
- package/src/client/components/core/AppStore.js +69 -0
- package/src/client/components/core/Auth.js +35 -37
- package/src/client/components/core/Badge.js +7 -13
- package/src/client/components/core/BtnIcon.js +15 -17
- package/src/client/components/core/CalendarCore.js +43 -64
- package/src/client/components/core/Chat.js +13 -15
- package/src/client/components/core/ClientEvents.js +87 -0
- package/src/client/components/core/ColorPaletteElement.js +309 -0
- package/src/client/components/core/Content.js +17 -14
- package/src/client/components/core/Css.js +15 -71
- package/src/client/components/core/CssCore.js +12 -16
- package/src/client/components/core/D3Chart.js +4 -4
- package/src/client/components/core/Docs.js +64 -91
- package/src/client/components/core/DropDown.js +194 -96
- package/src/client/components/core/EventBus.js +92 -0
- package/src/client/components/core/EventsUI.js +14 -17
- package/src/client/components/core/FileExplorer.js +96 -228
- package/src/client/components/core/FullScreen.js +47 -75
- package/src/client/components/core/Input.js +24 -69
- package/src/client/components/core/Keyboard.js +26 -19
- package/src/client/components/core/KeyboardAvoidance.js +145 -0
- package/src/client/components/core/LoadingAnimation.js +25 -31
- package/src/client/components/core/LogIn.js +43 -43
- package/src/client/components/core/LogOut.js +25 -16
- package/src/client/components/core/Modal.js +462 -179
- package/src/client/components/core/NotificationManager.js +14 -18
- package/src/client/components/core/Panel.js +54 -51
- package/src/client/components/core/PanelForm.js +44 -144
- package/src/client/components/core/Polyhedron.js +110 -214
- package/src/client/components/core/PublicProfile.js +39 -32
- package/src/client/components/core/Recover.js +48 -44
- package/src/client/components/core/Responsive.js +88 -32
- package/src/client/components/core/RichText.js +9 -18
- package/src/client/components/core/Router.js +24 -3
- package/src/client/components/core/SearchBox.js +37 -37
- package/src/client/components/core/SignUp.js +39 -30
- package/src/client/components/core/SocketIo.js +112 -30
- package/src/client/components/core/SocketIoHandler.js +75 -0
- package/src/client/components/core/Stream.js +143 -95
- package/src/client/components/core/ToggleSwitch.js +8 -20
- package/src/client/components/core/ToolTip.js +5 -17
- package/src/client/components/core/Translate.js +56 -59
- package/src/client/components/core/Validator.js +26 -16
- package/src/client/components/core/Wallet.js +15 -26
- package/src/client/components/core/Webhook.js +40 -7
- package/src/client/components/core/Worker.js +163 -27
- package/src/client/components/core/windowGetDimensions.js +7 -7
- package/src/client/components/cryptokoyn/{MenuCryptokoyn.js → AppShellCryptokoyn.js} +59 -59
- package/src/client/components/cryptokoyn/AppStoreCryptokoyn.js +5 -0
- package/src/client/components/cryptokoyn/CssCryptokoyn.js +15 -15
- package/src/client/components/cryptokoyn/LogInCryptokoyn.js +9 -7
- package/src/client/components/cryptokoyn/LogOutCryptokoyn.js +8 -6
- package/src/client/components/cryptokoyn/RouterCryptokoyn.js +37 -0
- package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +4 -4
- package/src/client/components/cryptokoyn/SignUpCryptokoyn.js +6 -4
- package/src/client/components/cryptokoyn/SocketIoCryptokoyn.js +3 -51
- package/src/client/components/cyberia/InstanceEngineCyberia.js +781 -0
- package/src/client/components/cyberia/MapEngineCyberia.js +1836 -2
- package/src/client/components/cyberia/ObjectLayerEngine.js +19 -0
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +1220 -99
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +252 -316
- package/src/client/components/cyberia-portal/{MenuCyberiaPortal.js → AppShellCyberiaPortal.js} +136 -103
- package/src/client/components/cyberia-portal/AppStoreCyberiaPortal.js +5 -0
- package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +462 -32
- package/src/client/components/cyberia-portal/CssCyberiaPortal.js +15 -15
- package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +9 -7
- package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +8 -6
- package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +4 -4
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +60 -0
- package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +4 -4
- package/src/client/components/cyberia-portal/SignUpCyberiaPortal.js +6 -4
- package/src/client/components/cyberia-portal/SocketIoCyberiaPortal.js +3 -49
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -4
- package/src/client/components/default/{MenuDefault.js → AppShellDefault.js} +91 -91
- package/src/client/components/default/AppStoreDefault.js +5 -0
- package/src/client/components/default/CssDefault.js +12 -12
- package/src/client/components/default/LogInDefault.js +9 -7
- package/src/client/components/default/LogOutDefault.js +8 -6
- package/src/client/components/default/RouterDefault.js +47 -0
- package/src/client/components/default/SettingsDefault.js +4 -4
- package/src/client/components/default/SignUpDefault.js +6 -4
- package/src/client/components/default/SocketIoDefault.js +3 -51
- package/src/client/components/default/TranslateDefault.js +3 -3
- package/src/client/components/itemledger/{MenuItemledger.js → AppShellItemledger.js} +59 -59
- package/src/client/components/itemledger/AppStoreItemledger.js +5 -0
- package/src/client/components/itemledger/CssItemledger.js +15 -15
- package/src/client/components/itemledger/LogInItemledger.js +9 -7
- package/src/client/components/itemledger/LogOutItemledger.js +8 -6
- package/src/client/components/itemledger/RouterItemledger.js +38 -0
- package/src/client/components/itemledger/SettingsItemledger.js +4 -4
- package/src/client/components/itemledger/SignUpItemledger.js +6 -4
- package/src/client/components/itemledger/SocketIoItemledger.js +3 -51
- package/src/client/components/itemledger/TranslateItemledger.js +3 -3
- package/src/client/components/underpost/{MenuUnderpost.js → AppShellUnderpost.js} +92 -92
- package/src/client/components/underpost/AppStoreUnderpost.js +5 -0
- package/src/client/components/underpost/CssUnderpost.js +14 -14
- package/src/client/components/underpost/CyberpunkBloggerUnderpost.js +4 -4
- package/src/client/components/underpost/DocumentSearchProvider.js +1 -1
- package/src/client/components/underpost/LabGalleryUnderpost.js +12 -15
- package/src/client/components/underpost/LogInUnderpost.js +9 -7
- package/src/client/components/underpost/LogOutUnderpost.js +8 -6
- package/src/client/components/underpost/RouterUnderpost.js +45 -0
- package/src/client/components/underpost/SettingsUnderpost.js +4 -4
- package/src/client/components/underpost/SignUpUnderpost.js +6 -4
- package/src/client/components/underpost/SocketIoUnderpost.js +3 -51
- package/src/client/components/underpost/TranslateUnderpost.js +4 -4
- package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +235 -0
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +443 -0
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +417 -0
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +313 -0
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +260 -0
- package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +241 -0
- package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +300 -0
- package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +279 -0
- package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +206 -0
- package/src/client/public/cyberia-docs/ROADMAP.md +240 -0
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +732 -0
- package/src/client/services/atlas-sprite-sheet/atlas-sprite-sheet.service.js +14 -20
- package/src/client/services/core/core.service.js +35 -55
- package/src/client/services/crypto/crypto.service.js +8 -13
- package/src/client/services/cyberia-action/cyberia-action.service.js +99 -0
- package/src/client/services/cyberia-dialogue/cyberia-dialogue.service.js +99 -0
- package/src/client/services/cyberia-entity/cyberia-entity.management.js +57 -0
- package/src/client/services/cyberia-entity/cyberia-entity.service.js +99 -0
- package/src/client/services/cyberia-instance/cyberia-instance.management.js +194 -0
- package/src/client/services/cyberia-instance/cyberia-instance.service.js +116 -0
- package/src/client/services/cyberia-instance-conf/cyberia-instance-conf.service.js +99 -0
- package/src/client/services/cyberia-map/cyberia-map.management.js +193 -0
- package/src/client/services/cyberia-map/cyberia-map.service.js +120 -0
- package/src/client/services/cyberia-quest/cyberia-quest.service.js +99 -0
- package/src/client/services/cyberia-quest-progress/cyberia-quest-progress.service.js +99 -0
- package/src/client/services/default/default.management.js +159 -267
- package/src/client/services/default/default.service.js +10 -16
- package/src/client/services/document/document.service.js +14 -19
- package/src/client/services/file/file.service.js +8 -13
- package/src/client/services/instance/instance.management.js +6 -6
- package/src/client/services/instance/instance.service.js +10 -15
- package/src/client/services/ipfs/ipfs.service.js +14 -40
- package/src/client/services/object-layer/object-layer.management.js +14 -14
- package/src/client/services/object-layer/object-layer.service.js +39 -24
- package/src/client/services/object-layer-render-frames/object-layer-render-frames.service.js +10 -16
- package/src/client/services/test/test.service.js +8 -13
- package/src/client/services/user/guest.service.js +86 -0
- package/src/client/services/user/user.management.js +6 -6
- package/src/client/services/user/user.service.js +14 -20
- package/src/client/ssr/body/404.js +3 -3
- package/src/client/ssr/body/500.js +3 -3
- package/src/client/ssr/body/CacheControl.js +5 -2
- package/src/client/ssr/body/DefaultSplashScreen.js +19 -12
- package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +13 -6
- package/src/client/ssr/head/PwaItemledger.js +197 -60
- package/src/client/ssr/mailer/DefaultRecoverEmail.js +19 -20
- package/src/client/ssr/mailer/DefaultVerifyEmail.js +15 -16
- package/src/client/ssr/offline/Maintenance.js +12 -11
- package/src/client/ssr/offline/NoNetworkConnection.js +3 -3
- package/src/client/ssr/pages/CyberiaServerMetrics.js +1 -1
- package/src/client/ssr/pages/Test.js +2 -2
- package/src/client/sw/core.sw.js +212 -0
- package/src/grpc/cyberia/grpc-server.js +642 -0
- package/src/index.js +24 -1
- package/src/runtime/cyberia-client/Dockerfile +80 -0
- package/src/runtime/cyberia-server/Dockerfile +37 -0
- package/src/runtime/express/Dockerfile +5 -1
- package/src/runtime/express/Express.js +18 -1
- package/src/runtime/lampp/Dockerfile +17 -5
- package/src/runtime/lampp/Lampp.js +27 -4
- package/src/runtime/wp/Dockerfile +62 -0
- package/src/runtime/wp/Wp.js +639 -0
- package/src/server/atlas-sprite-sheet-generator.js +4 -2
- package/src/server/auth.js +24 -1
- package/src/server/backup.js +37 -9
- package/src/server/client-build-docs.js +52 -46
- package/src/server/client-build.js +356 -82
- package/src/server/client-formatted.js +140 -57
- package/src/server/conf.js +29 -13
- package/src/server/cron.js +25 -23
- package/src/server/data-query.js +32 -20
- package/src/server/dns.js +24 -1
- package/src/server/ipfs-client.js +253 -89
- package/src/server/object-layer.js +150 -114
- package/src/server/peer.js +8 -0
- package/src/server/process.js +13 -27
- package/src/server/runtime.js +25 -1
- package/src/server/semantic-layer-generator-floor.js +319 -0
- package/src/server/semantic-layer-generator-resource.js +259 -0
- package/src/server/semantic-layer-generator-skin.js +1164 -0
- package/src/server/semantic-layer-generator.js +211 -542
- package/src/server/shape-generator.js +108 -0
- package/src/server/start.js +19 -5
- package/src/server/valkey.js +141 -235
- 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/tsconfig.docs.json +15 -0
- package/typedoc.dd-cyberia.json +29 -0
- package/typedoc.json +29 -0
- package/WHITE-PAPER.md +0 -1540
- package/hardhat/README.md +0 -531
- package/hardhat/WHITE-PAPER.md +0 -1540
- package/jsdoc.dd-cyberia.json +0 -59
- package/jsdoc.json +0 -59
- package/src/api/object-layer/README.md +0 -347
- package/src/client/components/core/ColorPalette.js +0 -5267
- package/src/client/components/core/JoyStick.js +0 -80
- package/src/client/components/cryptokoyn/CommonCryptokoyn.js +0 -29
- package/src/client/components/cryptokoyn/ElementsCryptokoyn.js +0 -38
- package/src/client/components/cryptokoyn/RoutesCryptokoyn.js +0 -39
- package/src/client/components/cyberia-portal/ElementsCyberiaPortal.js +0 -38
- package/src/client/components/cyberia-portal/RoutesCyberiaPortal.js +0 -58
- package/src/client/components/cyberia-portal/ServerCyberiaPortal.js +0 -136
- package/src/client/components/default/ElementsDefault.js +0 -38
- package/src/client/components/default/RoutesDefault.js +0 -49
- package/src/client/components/itemledger/CommonItemledger.js +0 -29
- package/src/client/components/itemledger/ElementsItemledger.js +0 -38
- package/src/client/components/itemledger/RoutesItemledger.js +0 -40
- package/src/client/components/underpost/CommonUnderpost.js +0 -29
- package/src/client/components/underpost/ElementsUnderpost.js +0 -38
- package/src/client/components/underpost/RoutesUnderpost.js +0 -47
- package/src/client/sw/default.sw.js +0 -127
- package/src/client/sw/template.sw.js +0 -84
- 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/src/cli/db.js
CHANGED
|
@@ -13,23 +13,9 @@ import fs from 'fs-extra';
|
|
|
13
13
|
import { DataBaseProvider } from '../db/DataBaseProvider.js';
|
|
14
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
|
-
* Redacts credentials from shell command strings before logging.
|
|
20
|
-
* Masks passwords in `-p<password>`, `--password=<password>`, and `-P <password>` patterns.
|
|
21
|
-
* @param {string} cmd - The raw command string.
|
|
22
|
-
* @memberof UnderpostDB
|
|
23
|
-
* @returns {string} The command with credentials replaced by `***`.
|
|
24
|
-
*/
|
|
25
|
-
const sanitizeCommand = (cmd) => {
|
|
26
|
-
if (typeof cmd !== 'string') return cmd;
|
|
27
|
-
return cmd
|
|
28
|
-
.replace(/-p['"]?[^\s'"]+/g, '-p***')
|
|
29
|
-
.replace(/--password=['"]?[^\s'"]+/g, '--password=***')
|
|
30
|
-
.replace(/-P\s+['"]?[^\s'"]+/g, '-P ***');
|
|
31
|
-
};
|
|
32
|
-
|
|
33
19
|
/**
|
|
34
20
|
* Constants for database operations
|
|
35
21
|
* @constant {number} MAX_BACKUP_RETENTION - Maximum number of backups to retain
|
|
@@ -98,132 +84,6 @@ class UnderpostDB {
|
|
|
98
84
|
* @memberof UnderpostDB
|
|
99
85
|
*/
|
|
100
86
|
static API = {
|
|
101
|
-
/**
|
|
102
|
-
* Helper: Gets filtered pods based on criteria.
|
|
103
|
-
* @method _getFilteredPods
|
|
104
|
-
* @memberof UnderpostDB
|
|
105
|
-
* @param {Object} criteria - Filter criteria.
|
|
106
|
-
* @param {string} [criteria.podNames] - Comma-separated pod name patterns.
|
|
107
|
-
* @param {string} [criteria.namespace='default'] - Kubernetes namespace.
|
|
108
|
-
* @param {string} [criteria.deployId] - Deployment ID pattern.
|
|
109
|
-
* @return {Array<PodInfo>} Filtered pod list.
|
|
110
|
-
*/
|
|
111
|
-
_getFilteredPods(criteria = {}) {
|
|
112
|
-
const { podNames, namespace = 'default', deployId } = criteria;
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
// Get all pods using Underpost.deploy.get
|
|
116
|
-
let pods = Underpost.deploy.get(deployId || '', 'pods', namespace);
|
|
117
|
-
|
|
118
|
-
// Filter by pod names if specified
|
|
119
|
-
if (podNames) {
|
|
120
|
-
const patterns = podNames.split(',').map((p) => p.trim());
|
|
121
|
-
pods = pods.filter((pod) => {
|
|
122
|
-
return patterns.some((pattern) => {
|
|
123
|
-
// Support wildcards
|
|
124
|
-
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
125
|
-
return regex.test(pod.NAME);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
logger.info(`Found ${pods.length} pod(s) matching criteria`, { criteria, podNames: pods.map((p) => p.NAME) });
|
|
131
|
-
return pods;
|
|
132
|
-
} catch (error) {
|
|
133
|
-
logger.error('Error filtering pods', { error: error.message, criteria });
|
|
134
|
-
return [];
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Helper: Executes kubectl command with error handling.
|
|
140
|
-
* @method _executeKubectl
|
|
141
|
-
* @memberof UnderpostDB
|
|
142
|
-
* @param {string} command - kubectl command to execute.
|
|
143
|
-
* @param {Object} [options={}] - Execution options.
|
|
144
|
-
* @param {string} [options.context=''] - Command context for logging.
|
|
145
|
-
* @return {string|null} Command output or null on error.
|
|
146
|
-
*/
|
|
147
|
-
_executeKubectl(command, options = {}) {
|
|
148
|
-
const { context = '' } = options;
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
logger.info(`Executing kubectl command`, { command: sanitizeCommand(command), context });
|
|
152
|
-
return shellExec(command, { stdout: true, disableLog: true });
|
|
153
|
-
} catch (error) {
|
|
154
|
-
logger.error(`kubectl command failed`, { command: sanitizeCommand(command), error: error.message, context });
|
|
155
|
-
throw error;
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Helper: Copies file to pod.
|
|
161
|
-
* @method _copyToPod
|
|
162
|
-
* @memberof UnderpostDB
|
|
163
|
-
* @param {Object} params - Copy parameters.
|
|
164
|
-
* @param {string} params.sourcePath - Source file path.
|
|
165
|
-
* @param {string} params.podName - Target pod name.
|
|
166
|
-
* @param {string} params.namespace - Pod namespace.
|
|
167
|
-
* @param {string} params.destPath - Destination path in pod.
|
|
168
|
-
* @return {boolean} Success status.
|
|
169
|
-
*/
|
|
170
|
-
_copyToPod({ sourcePath, podName, namespace, destPath }) {
|
|
171
|
-
try {
|
|
172
|
-
const command = `sudo kubectl cp ${sourcePath} ${namespace}/${podName}:${destPath}`;
|
|
173
|
-
Underpost.db._executeKubectl(command, { context: `copy to pod ${podName}` });
|
|
174
|
-
return true;
|
|
175
|
-
} catch (error) {
|
|
176
|
-
logger.error('Failed to copy file to pod', { sourcePath, podName, destPath, error: error.message });
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
},
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Helper: Copies file from pod.
|
|
183
|
-
* @method _copyFromPod
|
|
184
|
-
* @memberof UnderpostDB
|
|
185
|
-
* @param {Object} params - Copy parameters.
|
|
186
|
-
* @param {string} params.podName - Source pod name.
|
|
187
|
-
* @param {string} params.namespace - Pod namespace.
|
|
188
|
-
* @param {string} params.sourcePath - Source path in pod.
|
|
189
|
-
* @param {string} params.destPath - Destination file path.
|
|
190
|
-
* @return {boolean} Success status.
|
|
191
|
-
*/
|
|
192
|
-
_copyFromPod({ podName, namespace, sourcePath, destPath }) {
|
|
193
|
-
try {
|
|
194
|
-
const command = `sudo kubectl cp ${namespace}/${podName}:${sourcePath} ${destPath}`;
|
|
195
|
-
Underpost.db._executeKubectl(command, { context: `copy from pod ${podName}` });
|
|
196
|
-
return true;
|
|
197
|
-
} catch (error) {
|
|
198
|
-
logger.error('Failed to copy file from pod', { podName, sourcePath, destPath, error: error.message });
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Helper: Executes command in pod.
|
|
205
|
-
* @method _execInPod
|
|
206
|
-
* @memberof UnderpostDB
|
|
207
|
-
* @param {Object} params - Execution parameters.
|
|
208
|
-
* @param {string} params.podName - Pod name.
|
|
209
|
-
* @param {string} params.namespace - Pod namespace.
|
|
210
|
-
* @param {string} params.command - Command to execute.
|
|
211
|
-
* @return {string|null} Command output or null.
|
|
212
|
-
*/
|
|
213
|
-
_execInPod({ podName, namespace, command }) {
|
|
214
|
-
try {
|
|
215
|
-
const kubectlCmd = `sudo kubectl exec -n ${namespace} -i ${podName} -- sh -c "${command}"`;
|
|
216
|
-
return Underpost.db._executeKubectl(kubectlCmd, { context: `exec in pod ${podName}` });
|
|
217
|
-
} catch (error) {
|
|
218
|
-
logger.error('Failed to execute command in pod', {
|
|
219
|
-
podName,
|
|
220
|
-
command: sanitizeCommand(command),
|
|
221
|
-
error: error.message,
|
|
222
|
-
});
|
|
223
|
-
throw error;
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
|
|
227
87
|
/**
|
|
228
88
|
* Helper: Resolves the latest backup timestamp from an existing backup directory.
|
|
229
89
|
* Scans the directory for numeric (epoch) sub-folders and returns the most recent one.
|
|
@@ -239,76 +99,6 @@ class UnderpostDB {
|
|
|
239
99
|
return entries.sort((a, b) => parseInt(b) - parseInt(a))[0];
|
|
240
100
|
},
|
|
241
101
|
|
|
242
|
-
/**
|
|
243
|
-
* Helper: Manages Git repository for backups.
|
|
244
|
-
* @method _manageGitRepo
|
|
245
|
-
* @memberof UnderpostDB
|
|
246
|
-
* @param {Object} params - Git parameters.
|
|
247
|
-
* @param {string} params.repoName - Repository name.
|
|
248
|
-
* @param {string} params.operation - Operation (clone, pull, commit, push).
|
|
249
|
-
* @param {string} [params.message=''] - Commit message.
|
|
250
|
-
* @param {boolean} [params.forceClone=false] - Force remove and re-clone repository.
|
|
251
|
-
* @return {boolean} Success status.
|
|
252
|
-
*/
|
|
253
|
-
_manageGitRepo({ repoName, operation, message = '', forceClone = false }) {
|
|
254
|
-
try {
|
|
255
|
-
const username = process.env.GITHUB_USERNAME;
|
|
256
|
-
if (!username) {
|
|
257
|
-
logger.error('GITHUB_USERNAME environment variable not set');
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const repoPath = `../${repoName}`;
|
|
262
|
-
|
|
263
|
-
switch (operation) {
|
|
264
|
-
case 'clone':
|
|
265
|
-
if (forceClone && fs.existsSync(repoPath)) {
|
|
266
|
-
logger.info(`Force clone enabled, removing existing repository: ${repoName}`);
|
|
267
|
-
fs.removeSync(repoPath);
|
|
268
|
-
}
|
|
269
|
-
if (!fs.existsSync(repoPath)) {
|
|
270
|
-
shellExec(`cd .. && underpost clone ${username}/${repoName}`);
|
|
271
|
-
logger.info(`Cloned repository: ${repoName}`);
|
|
272
|
-
}
|
|
273
|
-
break;
|
|
274
|
-
|
|
275
|
-
case 'pull':
|
|
276
|
-
if (fs.existsSync(repoPath)) {
|
|
277
|
-
shellExec(`cd ${repoPath} && git checkout . && git clean -f -d`);
|
|
278
|
-
shellExec(`cd ${repoPath} && underpost pull . ${username}/${repoName}`, {
|
|
279
|
-
silent: true,
|
|
280
|
-
});
|
|
281
|
-
logger.info(`Pulled repository: ${repoName}`);
|
|
282
|
-
}
|
|
283
|
-
break;
|
|
284
|
-
|
|
285
|
-
case 'commit':
|
|
286
|
-
if (fs.existsSync(repoPath)) {
|
|
287
|
-
shellExec(`cd ${repoPath} && git add .`);
|
|
288
|
-
shellExec(`underpost cmt ${repoPath} backup '' '${message}'`);
|
|
289
|
-
logger.info(`Committed to repository: ${repoName}`, { message });
|
|
290
|
-
}
|
|
291
|
-
break;
|
|
292
|
-
|
|
293
|
-
case 'push':
|
|
294
|
-
if (fs.existsSync(repoPath)) {
|
|
295
|
-
shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName}`, { silent: true });
|
|
296
|
-
logger.info(`Pushed repository: ${repoName}`);
|
|
297
|
-
}
|
|
298
|
-
break;
|
|
299
|
-
|
|
300
|
-
default:
|
|
301
|
-
logger.warn(`Unknown git operation: ${operation}`);
|
|
302
|
-
return false;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return true;
|
|
306
|
-
} catch (error) {
|
|
307
|
-
logger.error(`Git operation failed`, { repoName, operation, error: error.message });
|
|
308
|
-
return false;
|
|
309
|
-
}
|
|
310
|
-
},
|
|
311
|
-
|
|
312
102
|
/**
|
|
313
103
|
* Helper: Performs MariaDB import operation.
|
|
314
104
|
* @method _importMariaDB
|
|
@@ -329,8 +119,20 @@ class UnderpostDB {
|
|
|
329
119
|
|
|
330
120
|
logger.info('Importing MariaDB database', { podName, dbName });
|
|
331
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
|
+
|
|
332
134
|
// Remove existing SQL file in container
|
|
333
|
-
Underpost.
|
|
135
|
+
Underpost.kubectl.exec({
|
|
334
136
|
podName,
|
|
335
137
|
namespace,
|
|
336
138
|
command: `rm -rf ${containerSqlPath}`,
|
|
@@ -338,7 +140,7 @@ class UnderpostDB {
|
|
|
338
140
|
|
|
339
141
|
// Copy SQL file to pod
|
|
340
142
|
if (
|
|
341
|
-
!Underpost.
|
|
143
|
+
!Underpost.kubectl.cpTo({
|
|
342
144
|
sourcePath: sqlPath,
|
|
343
145
|
podName,
|
|
344
146
|
namespace,
|
|
@@ -348,15 +150,9 @@ class UnderpostDB {
|
|
|
348
150
|
return false;
|
|
349
151
|
}
|
|
350
152
|
|
|
351
|
-
// Create database if it doesn't exist
|
|
352
|
-
Underpost.db._executeKubectl(
|
|
353
|
-
`kubectl exec -n ${namespace} -i ${podName} -- mariadb -p${password} -e 'CREATE DATABASE IF NOT EXISTS ${dbName};'`,
|
|
354
|
-
{ context: `create database ${dbName}` },
|
|
355
|
-
);
|
|
356
|
-
|
|
357
153
|
// Import SQL file
|
|
358
154
|
const importCmd = `mariadb -u ${user} -p${password} ${dbName} < ${containerSqlPath}`;
|
|
359
|
-
Underpost.
|
|
155
|
+
Underpost.kubectl.exec({ podName, namespace, command: importCmd });
|
|
360
156
|
|
|
361
157
|
logger.info('Successfully imported MariaDB database', { podName, dbName });
|
|
362
158
|
return true;
|
|
@@ -387,7 +183,7 @@ class UnderpostDB {
|
|
|
387
183
|
logger.info('Exporting MariaDB database', { podName, dbName });
|
|
388
184
|
|
|
389
185
|
// Remove existing SQL file in container
|
|
390
|
-
Underpost.
|
|
186
|
+
Underpost.kubectl.exec({
|
|
391
187
|
podName,
|
|
392
188
|
namespace,
|
|
393
189
|
command: `rm -rf ${containerSqlPath}`,
|
|
@@ -395,11 +191,11 @@ class UnderpostDB {
|
|
|
395
191
|
|
|
396
192
|
// Dump database
|
|
397
193
|
const dumpCmd = `mariadb-dump --user=${user} --password=${password} --lock-tables ${dbName} > ${containerSqlPath}`;
|
|
398
|
-
Underpost.
|
|
194
|
+
Underpost.kubectl.exec({ podName, namespace, command: dumpCmd });
|
|
399
195
|
|
|
400
196
|
// Copy SQL file from pod
|
|
401
197
|
if (
|
|
402
|
-
!Underpost.
|
|
198
|
+
!Underpost.kubectl.cpFrom({
|
|
403
199
|
podName,
|
|
404
200
|
namespace,
|
|
405
201
|
sourcePath: containerSqlPath,
|
|
@@ -442,8 +238,18 @@ class UnderpostDB {
|
|
|
442
238
|
|
|
443
239
|
logger.info('Importing MongoDB database', { podName, dbName });
|
|
444
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
|
+
|
|
445
251
|
// Remove existing BSON directory in container
|
|
446
|
-
Underpost.
|
|
252
|
+
Underpost.kubectl.exec({
|
|
447
253
|
podName,
|
|
448
254
|
namespace,
|
|
449
255
|
command: `rm -rf ${containerBsonPath}`,
|
|
@@ -451,7 +257,7 @@ class UnderpostDB {
|
|
|
451
257
|
|
|
452
258
|
// Copy BSON directory to pod
|
|
453
259
|
if (
|
|
454
|
-
!Underpost.
|
|
260
|
+
!Underpost.kubectl.cpTo({
|
|
455
261
|
sourcePath: bsonPath,
|
|
456
262
|
podName,
|
|
457
263
|
namespace,
|
|
@@ -465,7 +271,7 @@ class UnderpostDB {
|
|
|
465
271
|
const restoreCmd = `mongorestore -d ${dbName} ${containerBsonPath}${drop ? ' --drop' : ''}${
|
|
466
272
|
preserveUUID ? ' --preserveUUID' : ''
|
|
467
273
|
}`;
|
|
468
|
-
Underpost.
|
|
274
|
+
Underpost.kubectl.exec({ podName, namespace, command: restoreCmd });
|
|
469
275
|
|
|
470
276
|
logger.info('Successfully imported MongoDB database', { podName, dbName });
|
|
471
277
|
return true;
|
|
@@ -495,7 +301,7 @@ class UnderpostDB {
|
|
|
495
301
|
logger.info('Exporting MongoDB database', { podName, dbName, collections });
|
|
496
302
|
|
|
497
303
|
// Remove existing BSON directory in container
|
|
498
|
-
Underpost.
|
|
304
|
+
Underpost.kubectl.exec({
|
|
499
305
|
podName,
|
|
500
306
|
namespace,
|
|
501
307
|
command: `rm -rf ${containerBsonPath}`,
|
|
@@ -506,16 +312,16 @@ class UnderpostDB {
|
|
|
506
312
|
const collectionList = collections.split(',').map((c) => c.trim());
|
|
507
313
|
for (const collection of collectionList) {
|
|
508
314
|
const dumpCmd = `mongodump -d ${dbName} --collection ${collection} -o /`;
|
|
509
|
-
Underpost.
|
|
315
|
+
Underpost.kubectl.exec({ podName, namespace, command: dumpCmd });
|
|
510
316
|
}
|
|
511
317
|
} else {
|
|
512
318
|
const dumpCmd = `mongodump -d ${dbName} -o /`;
|
|
513
|
-
Underpost.
|
|
319
|
+
Underpost.kubectl.exec({ podName, namespace, command: dumpCmd });
|
|
514
320
|
}
|
|
515
321
|
|
|
516
322
|
// Copy BSON directory from pod
|
|
517
323
|
if (
|
|
518
|
-
!Underpost.
|
|
324
|
+
!Underpost.kubectl.cpFrom({
|
|
519
325
|
podName,
|
|
520
326
|
namespace,
|
|
521
327
|
sourcePath: containerBsonPath,
|
|
@@ -743,6 +549,7 @@ class UnderpostDB {
|
|
|
743
549
|
* @param {boolean} [options.k3s=false] - k3s cluster flag.
|
|
744
550
|
* @param {boolean} [options.kubeadm=false] - kubeadm cluster flag.
|
|
745
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.
|
|
746
553
|
* @return {Promise<void>} Resolves when operation is complete.
|
|
747
554
|
*/
|
|
748
555
|
async callback(
|
|
@@ -771,346 +578,420 @@ class UnderpostDB {
|
|
|
771
578
|
k3s: false,
|
|
772
579
|
kubeadm: false,
|
|
773
580
|
kind: false,
|
|
581
|
+
repoBackup: false,
|
|
774
582
|
},
|
|
775
583
|
) {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
+
}
|
|
792
609
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
+
});
|
|
799
627
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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;
|
|
807
637
|
}
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
638
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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();
|
|
815
643
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
644
|
+
for (const _deployId of deployList.split(',')) {
|
|
645
|
+
const deployId = _deployId.trim();
|
|
646
|
+
if (!deployId) continue;
|
|
819
647
|
|
|
820
|
-
|
|
648
|
+
logger.info('Processing deployment', { deployId });
|
|
821
649
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
650
|
+
/** @type {Object.<string, Object.<string, DatabaseConfig>>} */
|
|
651
|
+
const dbs = {};
|
|
652
|
+
const repoName = `engine-${deployId.includes('dd-') ? deployId.split('dd-')[1] : deployId}-cron-backups`;
|
|
825
653
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
+
}
|
|
832
660
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
+
}
|
|
851
680
|
}
|
|
852
681
|
}
|
|
853
682
|
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// Handle Git operations - execute only once per repository
|
|
857
|
-
if (!processedRepos.has(repoName)) {
|
|
858
|
-
logger.info('Processing Git operations for repository', { repoName, deployId });
|
|
859
|
-
if (options.git === true) {
|
|
860
|
-
Underpost.db._manageGitRepo({ repoName, operation: 'clone', forceClone: options.forceClone });
|
|
861
|
-
Underpost.db._manageGitRepo({ repoName, operation: 'pull' });
|
|
862
|
-
}
|
|
863
683
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
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' });
|
|
869
690
|
}
|
|
870
691
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
shellExec(`cd ${repoPath} && underpost cmt . reset ${nCommits}`);
|
|
878
|
-
shellExec(`cd ${repoPath} && git reset`);
|
|
879
|
-
shellExec(`cd ${repoPath} && git checkout .`);
|
|
880
|
-
shellExec(`cd ${repoPath} && git clean -f -d`);
|
|
881
|
-
shellExec(`cd ${repoPath} && underpost push . ${username}/${repoName} -f`);
|
|
882
|
-
} else {
|
|
883
|
-
if (!username) logger.error('GITHUB_USERNAME environment variable not set');
|
|
884
|
-
logger.warn('Repository not found for macro rollback', { repoPath });
|
|
885
|
-
}
|
|
886
|
-
}
|
|
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
|
+
}
|
|
887
698
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
+
}
|
|
893
715
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
+
}
|
|
898
721
|
|
|
899
|
-
|
|
900
|
-
|
|
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];
|
|
901
726
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
logger.info('Skipping already processed host/path', { dbName, host, path, deployId });
|
|
905
|
-
continue;
|
|
906
|
-
}
|
|
727
|
+
// Create unique identifier for host+path combination
|
|
728
|
+
const hostPathKey = `${deployId}:${host}:${path}`;
|
|
907
729
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
.map((h) => h.trim())
|
|
914
|
-
.includes(host)) ||
|
|
915
|
-
(options.paths &&
|
|
916
|
-
!options.paths
|
|
917
|
-
.split(',')
|
|
918
|
-
.map((p) => p.trim())
|
|
919
|
-
.includes(path))
|
|
920
|
-
) {
|
|
921
|
-
logger.info('Skipping database due to host/path filter', { dbName, host, path });
|
|
922
|
-
continue;
|
|
923
|
-
}
|
|
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
|
+
}
|
|
924
735
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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
|
+
}
|
|
929
752
|
|
|
930
|
-
|
|
753
|
+
if (!hostFolder) {
|
|
754
|
+
logger.warn('No hostFolder defined for database', { dbName, provider });
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
931
757
|
|
|
932
|
-
|
|
758
|
+
logger.info('Processing database', { hostFolder, provider, dbName, deployId });
|
|
933
759
|
|
|
934
|
-
|
|
760
|
+
const latestBackupTimestamp = Underpost.db._getLatestBackupTimestamp(`../${repoName}/${hostFolder}`);
|
|
935
761
|
|
|
936
|
-
|
|
937
|
-
const sqlContainerPath = `/home/${dbName}.sql`;
|
|
938
|
-
const fromPartsPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}-parths.json`;
|
|
939
|
-
const toSqlPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}.sql`;
|
|
940
|
-
const toNewSqlPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}.sql`;
|
|
941
|
-
const toBsonPath = `../${repoName}/${hostFolder}/${currentTimestamp}/${dbName}`;
|
|
942
|
-
const toNewBsonPath = `../${repoName}/${hostFolder}/${newBackupTimestamp}/${dbName}`;
|
|
762
|
+
dbs[provider][dbName].currentBackupTimestamp = latestBackupTimestamp;
|
|
943
763
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
}
|
|
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}`;
|
|
952
771
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
+
}
|
|
960
780
|
|
|
961
|
-
|
|
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
|
+
};
|
|
962
788
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
+
}
|
|
969
801
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
802
|
+
if (targetPods.length === 0) {
|
|
803
|
+
logger.warn('No pods found matching criteria', { provider, criteria: podCriteria });
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
974
806
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
+
}
|
|
991
827
|
} else {
|
|
992
|
-
logger.warn('
|
|
828
|
+
logger.warn('Could not detect primary pod, using first pod');
|
|
993
829
|
podsToProcess = [targetPods[0]];
|
|
994
830
|
}
|
|
995
|
-
} else {
|
|
996
|
-
logger.warn('Could not detect primary pod, using first pod');
|
|
997
|
-
podsToProcess = [targetPods[0]];
|
|
998
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]];
|
|
999
835
|
}
|
|
1000
|
-
} else {
|
|
1001
|
-
// For MariaDB or when allPods is true, limit to first pod unless allPods is true
|
|
1002
|
-
podsToProcess = options.allPods === true ? targetPods : [targetPods[0]];
|
|
1003
|
-
}
|
|
1004
836
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
837
|
+
logger.info(`Processing ${podsToProcess.length} pod(s) for ${provider}`, {
|
|
838
|
+
dbName,
|
|
839
|
+
pods: podsToProcess.map((p) => p.NAME),
|
|
840
|
+
});
|
|
1009
841
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
842
|
+
let exportSucceeded = false;
|
|
843
|
+
|
|
844
|
+
// Process each pod
|
|
845
|
+
for (const pod of podsToProcess) {
|
|
846
|
+
logger.info('Processing pod', { podName: pod.NAME, node: pod.NODE, status: pod.STATUS });
|
|
847
|
+
|
|
848
|
+
switch (provider) {
|
|
849
|
+
case 'mariadb': {
|
|
850
|
+
if (options.stats === true) {
|
|
851
|
+
const stats = Underpost.db._getMariaDBStats({
|
|
852
|
+
podName: pod.NAME,
|
|
853
|
+
namespace,
|
|
854
|
+
dbName,
|
|
855
|
+
user,
|
|
856
|
+
password,
|
|
857
|
+
});
|
|
858
|
+
if (stats) {
|
|
859
|
+
Underpost.db._displayStats({ provider, dbName, stats });
|
|
860
|
+
}
|
|
1026
861
|
}
|
|
1027
|
-
}
|
|
1028
862
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
863
|
+
if (options.import === true) {
|
|
864
|
+
Underpost.db._importMariaDB({
|
|
865
|
+
pod,
|
|
866
|
+
namespace,
|
|
867
|
+
dbName,
|
|
868
|
+
user,
|
|
869
|
+
password,
|
|
870
|
+
sqlPath: toSqlPath,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
1039
873
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
874
|
+
if (options.export === true) {
|
|
875
|
+
const outputPath = options.outPath || toNewSqlPath;
|
|
876
|
+
const success = await Underpost.db._exportMariaDB({
|
|
877
|
+
pod,
|
|
878
|
+
namespace,
|
|
879
|
+
dbName,
|
|
880
|
+
user,
|
|
881
|
+
password,
|
|
882
|
+
outputPath,
|
|
883
|
+
});
|
|
884
|
+
exportSucceeded = exportSucceeded || success;
|
|
885
|
+
}
|
|
886
|
+
break;
|
|
1050
887
|
}
|
|
1051
|
-
break;
|
|
1052
|
-
}
|
|
1053
888
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
889
|
+
case 'mongoose': {
|
|
890
|
+
if (options.stats === true) {
|
|
891
|
+
const stats = Underpost.db._getMongoStats({
|
|
892
|
+
podName: pod.NAME,
|
|
893
|
+
namespace,
|
|
894
|
+
dbName,
|
|
895
|
+
});
|
|
896
|
+
if (stats) {
|
|
897
|
+
Underpost.db._displayStats({ provider, dbName, stats });
|
|
898
|
+
}
|
|
1063
899
|
}
|
|
1064
|
-
}
|
|
1065
900
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
901
|
+
if (options.import === true) {
|
|
902
|
+
const bsonPath = options.outPath || toBsonPath;
|
|
903
|
+
Underpost.db._importMongoDB({
|
|
904
|
+
pod,
|
|
905
|
+
namespace,
|
|
906
|
+
dbName,
|
|
907
|
+
bsonPath,
|
|
908
|
+
drop: options.drop,
|
|
909
|
+
preserveUUID: options.preserveUUID,
|
|
910
|
+
});
|
|
911
|
+
}
|
|
1077
912
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
913
|
+
if (options.export === true) {
|
|
914
|
+
const outputPath = options.outPath || toNewBsonPath;
|
|
915
|
+
const success = Underpost.db._exportMongoDB({
|
|
916
|
+
pod,
|
|
917
|
+
namespace,
|
|
918
|
+
dbName,
|
|
919
|
+
outputPath,
|
|
920
|
+
collections: options.collections,
|
|
921
|
+
});
|
|
922
|
+
exportSucceeded = exportSucceeded || success;
|
|
923
|
+
}
|
|
924
|
+
break;
|
|
1087
925
|
}
|
|
1088
|
-
|
|
926
|
+
|
|
927
|
+
default:
|
|
928
|
+
logger.warn('Unsupported database provider', { provider });
|
|
929
|
+
break;
|
|
1089
930
|
}
|
|
931
|
+
}
|
|
1090
932
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
break;
|
|
933
|
+
if (options.export === true && exportSucceeded === true) {
|
|
934
|
+
Underpost.db._enforceBackupRetention(`../${repoName}/${hostFolder}`);
|
|
1094
935
|
}
|
|
936
|
+
|
|
937
|
+
// Mark this host+path combination as processed
|
|
938
|
+
processedHostPaths.add(hostPathKey);
|
|
1095
939
|
}
|
|
940
|
+
}
|
|
1096
941
|
|
|
1097
|
-
|
|
1098
|
-
|
|
942
|
+
// Commit and push to Git if enabled - execute only once per repository
|
|
943
|
+
if (options.export === true && options.git === true && !processedRepos.has(`${repoName}-committed`)) {
|
|
944
|
+
const commitMessage = `${new Date(newBackupTimestamp).toLocaleDateString()} ${new Date(
|
|
945
|
+
newBackupTimestamp,
|
|
946
|
+
).toLocaleTimeString()}`;
|
|
947
|
+
Underpost.repo.manageBackupRepo({ repoName, operation: 'commit', message: commitMessage });
|
|
948
|
+
Underpost.repo.manageBackupRepo({ repoName, operation: 'push' });
|
|
949
|
+
processedRepos.add(`${repoName}-committed`);
|
|
1099
950
|
}
|
|
1100
951
|
}
|
|
1101
952
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
).toLocaleTimeString()}`;
|
|
1107
|
-
Underpost.db._manageGitRepo({ repoName, operation: 'commit', message: commitMessage });
|
|
1108
|
-
Underpost.db._manageGitRepo({ repoName, operation: 'push' });
|
|
1109
|
-
processedRepos.add(`${repoName}-committed`);
|
|
1110
|
-
}
|
|
953
|
+
logger.info('Database operation completed successfully');
|
|
954
|
+
} catch (error) {
|
|
955
|
+
logger.error('Database operation failed', { error: error.message });
|
|
956
|
+
throw error;
|
|
1111
957
|
}
|
|
958
|
+
},
|
|
959
|
+
/**
|
|
960
|
+
* Helper: Removes old timestamp backup folders and keeps only the newest ones.
|
|
961
|
+
* @method _enforceBackupRetention
|
|
962
|
+
* @memberof UnderpostDB
|
|
963
|
+
* @param {string} backupDir - Path to host-folder backup directory.
|
|
964
|
+
* @param {number} [maxRetention=MAX_BACKUP_RETENTION] - Maximum folders to keep.
|
|
965
|
+
* @return {number} Number of removed backup folders.
|
|
966
|
+
*/
|
|
967
|
+
_enforceBackupRetention(backupDir, maxRetention = MAX_BACKUP_RETENTION) {
|
|
968
|
+
try {
|
|
969
|
+
if (!fs.existsSync(backupDir)) return 0;
|
|
970
|
+
|
|
971
|
+
const timestamps = fs
|
|
972
|
+
.readdirSync(backupDir)
|
|
973
|
+
.filter((entry) => /^\d+$/.test(entry))
|
|
974
|
+
.sort((a, b) => parseInt(b, 10) - parseInt(a, 10));
|
|
975
|
+
|
|
976
|
+
if (timestamps.length <= maxRetention) return 0;
|
|
1112
977
|
|
|
1113
|
-
|
|
978
|
+
const staleTimestamps = timestamps.slice(maxRetention);
|
|
979
|
+
staleTimestamps.forEach((timestamp) => {
|
|
980
|
+
fs.removeSync(`${backupDir}/${timestamp}`);
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
logger.info('Pruned old backup timestamp folders', {
|
|
984
|
+
backupDir,
|
|
985
|
+
kept: maxRetention,
|
|
986
|
+
removed: staleTimestamps.length,
|
|
987
|
+
removedTimestamps: staleTimestamps,
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
return staleTimestamps.length;
|
|
991
|
+
} catch (error) {
|
|
992
|
+
logger.error('Failed to enforce backup retention', { backupDir, maxRetention, error: error.message });
|
|
993
|
+
return 0;
|
|
994
|
+
}
|
|
1114
995
|
},
|
|
1115
996
|
|
|
1116
997
|
/**
|
|
@@ -1122,6 +1003,8 @@ class UnderpostDB {
|
|
|
1122
1003
|
* @param {string} [deployId=process.env.DEFAULT_DEPLOY_ID] - The deployment ID.
|
|
1123
1004
|
* @param {string} [host=process.env.DEFAULT_DEPLOY_HOST] - The host identifier.
|
|
1124
1005
|
* @param {string} [path=process.env.DEFAULT_DEPLOY_PATH] - The path identifier.
|
|
1006
|
+
* @param {object} [options] - Options.
|
|
1007
|
+
* @param {boolean} [options.dev=false] - Development mode flag.
|
|
1125
1008
|
* @return {Promise<void>} Resolves when metadata creation is complete.
|
|
1126
1009
|
* @throws {Error} If database configuration is invalid or connection fails.
|
|
1127
1010
|
*/
|
|
@@ -1129,161 +1012,181 @@ class UnderpostDB {
|
|
|
1129
1012
|
deployId = process.env.DEFAULT_DEPLOY_ID,
|
|
1130
1013
|
host = process.env.DEFAULT_DEPLOY_HOST,
|
|
1131
1014
|
path = process.env.DEFAULT_DEPLOY_PATH,
|
|
1015
|
+
options = { dev: false },
|
|
1132
1016
|
) {
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1017
|
+
try {
|
|
1018
|
+
loadCronDeployEnv();
|
|
1019
|
+
deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
|
|
1020
|
+
host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
|
|
1021
|
+
path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
|
|
1137
1022
|
|
|
1138
|
-
|
|
1023
|
+
logger.info('Creating cluster metadata', { deployId, host, path });
|
|
1139
1024
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1025
|
+
const env = 'production';
|
|
1026
|
+
const deployListPath = './engine-private/deploy/dd.router';
|
|
1142
1027
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1028
|
+
if (!fs.existsSync(deployListPath)) {
|
|
1029
|
+
logger.error('Deploy router file not found', { path: deployListPath });
|
|
1030
|
+
throw new Error(`Deploy router file not found: ${deployListPath}`);
|
|
1031
|
+
}
|
|
1147
1032
|
|
|
1148
|
-
|
|
1033
|
+
const deployList = fs.readFileSync(deployListPath, 'utf8').split(',');
|
|
1149
1034
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1035
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
1036
|
+
if (!fs.existsSync(confServerPath)) {
|
|
1037
|
+
logger.error('Server configuration not found', { path: confServerPath });
|
|
1038
|
+
throw new Error(`Server configuration not found: ${confServerPath}`);
|
|
1039
|
+
}
|
|
1155
1040
|
|
|
1156
|
-
|
|
1041
|
+
const { db } = loadConfServerJson(confServerPath, { resolve: true })[host][path];
|
|
1157
1042
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1043
|
+
const maxRetries = 5;
|
|
1044
|
+
const retryDelay = 3000;
|
|
1045
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1046
|
+
try {
|
|
1047
|
+
await DataBaseProvider.load({ apis: ['instance', 'cron'], host, path, db });
|
|
1048
|
+
break;
|
|
1049
|
+
} catch (err) {
|
|
1050
|
+
if (attempt === maxRetries) {
|
|
1051
|
+
logger.error('Failed to connect to database after retries', { attempts: maxRetries, error: err.message });
|
|
1052
|
+
throw err;
|
|
1053
|
+
}
|
|
1054
|
+
logger.warn('Database connection failed, retrying...', { attempt, maxRetries, error: err.message });
|
|
1055
|
+
await timer(retryDelay);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1160
1058
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1059
|
+
try {
|
|
1060
|
+
/** @type {import('../api/instance/instance.model.js').InstanceModel} */
|
|
1061
|
+
const Instance = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Instance;
|
|
1163
1062
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1063
|
+
await Instance.deleteMany();
|
|
1064
|
+
logger.info('Cleared existing instance metadata');
|
|
1166
1065
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1066
|
+
for (const _deployId of deployList) {
|
|
1067
|
+
const deployId = _deployId.trim();
|
|
1068
|
+
if (!deployId) continue;
|
|
1170
1069
|
|
|
1171
|
-
|
|
1070
|
+
logger.info('Processing deployment for metadata', { deployId });
|
|
1172
1071
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1072
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
1073
|
+
if (!fs.existsSync(confServerPath)) {
|
|
1074
|
+
logger.warn('Configuration not found for deployment', { deployId, path: confServerPath });
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1178
1077
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1078
|
+
const confServer = loadReplicas(deployId, loadConfServerJson(confServerPath, { resolve: true }));
|
|
1079
|
+
const router = await Underpost.deploy.routerFactory(deployId, env);
|
|
1080
|
+
const pathPortAssignmentData = await pathPortAssignmentFactory(deployId, router, confServer);
|
|
1182
1081
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1082
|
+
for (const host of Object.keys(confServer)) {
|
|
1083
|
+
for (const { path, port } of pathPortAssignmentData[host]) {
|
|
1084
|
+
if (!confServer[host][path]) continue;
|
|
1186
1085
|
|
|
1187
|
-
|
|
1086
|
+
const { client, runtime, apis, peer } = confServer[host][path];
|
|
1188
1087
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1088
|
+
// Save main instance
|
|
1089
|
+
{
|
|
1090
|
+
const body = {
|
|
1091
|
+
deployId,
|
|
1092
|
+
host,
|
|
1093
|
+
path,
|
|
1094
|
+
port,
|
|
1095
|
+
client,
|
|
1096
|
+
runtime,
|
|
1097
|
+
apis,
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
logger.info('Saving instance metadata', body);
|
|
1101
|
+
await new Instance(body).save();
|
|
1102
|
+
}
|
|
1200
1103
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1104
|
+
// Save peer instance if exists
|
|
1105
|
+
if (peer) {
|
|
1106
|
+
const body = {
|
|
1107
|
+
deployId,
|
|
1108
|
+
host,
|
|
1109
|
+
path: path === '/' ? '/peer' : `${path}/peer`,
|
|
1110
|
+
port: port + 1,
|
|
1111
|
+
runtime: 'nodejs',
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
logger.info('Saving peer instance metadata', body);
|
|
1115
|
+
await new Instance(body).save();
|
|
1116
|
+
}
|
|
1203
1117
|
}
|
|
1118
|
+
}
|
|
1204
1119
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1120
|
+
// Process additional instances
|
|
1121
|
+
const confInstancesPath = `./engine-private/conf/${deployId}/conf.instances.json`;
|
|
1122
|
+
if (fs.existsSync(confInstancesPath)) {
|
|
1123
|
+
const confInstances = JSON.parse(fs.readFileSync(confInstancesPath, 'utf8'));
|
|
1124
|
+
for (const instance of confInstances) {
|
|
1125
|
+
const { id, host, path, fromPort, metadata } = instance;
|
|
1126
|
+
const { runtime } = metadata;
|
|
1207
1127
|
const body = {
|
|
1208
1128
|
deployId,
|
|
1209
1129
|
host,
|
|
1210
|
-
path
|
|
1211
|
-
port:
|
|
1212
|
-
|
|
1130
|
+
path,
|
|
1131
|
+
port: fromPort,
|
|
1132
|
+
client: id,
|
|
1133
|
+
runtime,
|
|
1213
1134
|
};
|
|
1214
|
-
|
|
1215
|
-
logger.info('Saving peer instance metadata', body);
|
|
1135
|
+
logger.info('Saving additional instance metadata', body);
|
|
1216
1136
|
await new Instance(body).save();
|
|
1217
1137
|
}
|
|
1218
1138
|
}
|
|
1219
1139
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
if (fs.existsSync(confInstancesPath)) {
|
|
1224
|
-
const confInstances = JSON.parse(fs.readFileSync(confInstancesPath, 'utf8'));
|
|
1225
|
-
for (const instance of confInstances) {
|
|
1226
|
-
const { id, host, path, fromPort, metadata } = instance;
|
|
1227
|
-
const { runtime } = metadata;
|
|
1228
|
-
const body = {
|
|
1229
|
-
deployId,
|
|
1230
|
-
host,
|
|
1231
|
-
path,
|
|
1232
|
-
port: fromPort,
|
|
1233
|
-
client: id,
|
|
1234
|
-
runtime,
|
|
1235
|
-
};
|
|
1236
|
-
logger.info('Saving additional instance metadata', body);
|
|
1237
|
-
await new Instance(body).save();
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
logger.error('Failed to create instance metadata', { error: error.message });
|
|
1142
|
+
throw error;
|
|
1240
1143
|
}
|
|
1241
|
-
} catch (error) {
|
|
1242
|
-
logger.error('Failed to create instance metadata', { error: error.message });
|
|
1243
|
-
throw error;
|
|
1244
|
-
}
|
|
1245
1144
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1145
|
+
try {
|
|
1146
|
+
const cronDeployPath = './engine-private/deploy/dd.cron';
|
|
1147
|
+
if (!fs.existsSync(cronDeployPath)) {
|
|
1148
|
+
logger.warn('Cron deploy file not found', { path: cronDeployPath });
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1252
1151
|
|
|
1253
|
-
|
|
1254
|
-
|
|
1152
|
+
const cronDeployId = fs.readFileSync(cronDeployPath, 'utf8').trim();
|
|
1153
|
+
const confCronPath = `./engine-private/conf/${cronDeployId}/conf.cron.json`;
|
|
1255
1154
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1155
|
+
if (!fs.existsSync(confCronPath)) {
|
|
1156
|
+
logger.warn('Cron configuration not found', { path: confCronPath });
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1260
1159
|
|
|
1261
|
-
|
|
1160
|
+
const confCron = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
|
|
1262
1161
|
|
|
1263
|
-
|
|
1162
|
+
await DataBaseProvider.load({ apis: ['cron'], host, path, db });
|
|
1264
1163
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1164
|
+
/** @type {import('../api/cron/cron.model.js').CronModel} */
|
|
1165
|
+
const Cron = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Cron;
|
|
1267
1166
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1167
|
+
await Cron.deleteMany();
|
|
1168
|
+
logger.info('Cleared existing cron metadata');
|
|
1270
1169
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1170
|
+
for (const jobId of Object.keys(confCron.jobs)) {
|
|
1171
|
+
const body = {
|
|
1172
|
+
jobId,
|
|
1173
|
+
deployId: Underpost.cron.getRelatedDeployIdList(jobId),
|
|
1174
|
+
expression: confCron.jobs[jobId].expression,
|
|
1175
|
+
enabled: confCron.jobs[jobId].enabled,
|
|
1176
|
+
};
|
|
1177
|
+
logger.info('Saving cron metadata', body);
|
|
1178
|
+
await new Cron(body).save();
|
|
1179
|
+
}
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
logger.error('Failed to create cron metadata', { error: error.message });
|
|
1280
1182
|
}
|
|
1183
|
+
|
|
1184
|
+
await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
|
|
1185
|
+
logger.info('Cluster metadata creation completed');
|
|
1281
1186
|
} catch (error) {
|
|
1282
|
-
logger.error('
|
|
1187
|
+
logger.error('Cluster metadata creation failed', { error: error.message });
|
|
1188
|
+
throw error;
|
|
1283
1189
|
}
|
|
1284
|
-
|
|
1285
|
-
await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
|
|
1286
|
-
logger.info('Cluster metadata creation completed');
|
|
1287
1190
|
},
|
|
1288
1191
|
|
|
1289
1192
|
/**
|
|
@@ -1297,6 +1200,7 @@ class UnderpostDB {
|
|
|
1297
1200
|
* @param {string} [options.hosts=''] - Comma-separated list of hosts to filter.
|
|
1298
1201
|
* @param {string} [options.paths=''] - Comma-separated list of paths to filter.
|
|
1299
1202
|
* @param {boolean} [options.dryRun=false] - If true, only reports what would be deleted.
|
|
1203
|
+
* @param {boolean} [options.dev=false] - Development mode flag.
|
|
1300
1204
|
* @return {Promise<void>} Resolves when clean operation is complete.
|
|
1301
1205
|
*/
|
|
1302
1206
|
async cleanFsCollection(
|
|
@@ -1305,203 +1209,220 @@ class UnderpostDB {
|
|
|
1305
1209
|
hosts: '',
|
|
1306
1210
|
paths: '',
|
|
1307
1211
|
dryRun: false,
|
|
1212
|
+
dev: false,
|
|
1308
1213
|
},
|
|
1309
1214
|
) {
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
// Load file.ref.json to know which models reference File
|
|
1316
|
-
const fileRefPath = './src/api/file/file.ref.json';
|
|
1317
|
-
if (!fs.existsSync(fileRefPath)) {
|
|
1318
|
-
logger.error('file.ref.json not found', { path: fileRefPath });
|
|
1319
|
-
return;
|
|
1320
|
-
}
|
|
1215
|
+
const firstDeployId = deployList !== 'dd' ? deployList.split(',')[0].trim() : '';
|
|
1216
|
+
try {
|
|
1217
|
+
loadCronDeployEnv();
|
|
1218
|
+
if (deployList === 'dd') deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8');
|
|
1321
1219
|
|
|
1322
|
-
|
|
1323
|
-
logger.info('Loaded file reference configuration', { apis: fileRefData.length });
|
|
1220
|
+
logger.info('Starting File collection cleanup', { deployList, options });
|
|
1324
1221
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1222
|
+
// Load file.ref.json to know which models reference File
|
|
1223
|
+
const fileRefPath = './src/api/file/file.ref.json';
|
|
1224
|
+
if (!fs.existsSync(fileRefPath)) {
|
|
1225
|
+
logger.error('file.ref.json not found', { path: fileRefPath });
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1328
1228
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1229
|
+
const fileRefData = JSON.parse(fs.readFileSync(fileRefPath, 'utf8'));
|
|
1230
|
+
logger.info('Loaded file reference configuration', { apis: fileRefData.length });
|
|
1331
1231
|
|
|
1332
|
-
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1232
|
+
// Filter hosts and paths if specified
|
|
1233
|
+
const filterHosts = options.hosts ? options.hosts.split(',').map((h) => h.trim()) : [];
|
|
1234
|
+
const filterPaths = options.paths ? options.paths.split(',').map((p) => p.trim()) : [];
|
|
1335
1235
|
|
|
1336
|
-
|
|
1236
|
+
// Track all connections to close them at the end
|
|
1237
|
+
const connectionsToClose = [];
|
|
1337
1238
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
logger.error('Configuration file not found', { path: confServerPath });
|
|
1342
|
-
continue;
|
|
1343
|
-
}
|
|
1239
|
+
for (const _deployId of deployList.split(',')) {
|
|
1240
|
+
const deployId = _deployId.trim();
|
|
1241
|
+
if (!deployId) continue;
|
|
1344
1242
|
|
|
1345
|
-
|
|
1243
|
+
logger.info('Processing deployment for File cleanup', { deployId });
|
|
1346
1244
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
if (
|
|
1245
|
+
// Load server configuration
|
|
1246
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
1247
|
+
if (!fs.existsSync(confServerPath)) {
|
|
1248
|
+
logger.error('Configuration file not found', { path: confServerPath });
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1350
1251
|
|
|
1351
|
-
|
|
1352
|
-
if (filterPaths.length > 0 && !filterPaths.includes(path)) continue;
|
|
1252
|
+
const confServer = loadConfServerJson(confServerPath, { resolve: true });
|
|
1353
1253
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1254
|
+
// Process each host+path combination
|
|
1255
|
+
for (const host of Object.keys(confServer)) {
|
|
1256
|
+
if (filterHosts.length > 0 && !filterHosts.includes(host)) continue;
|
|
1356
1257
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
logger.info('Skipping - no file api in configuration', { host, path });
|
|
1360
|
-
continue;
|
|
1361
|
-
}
|
|
1258
|
+
for (const path of Object.keys(confServer[host])) {
|
|
1259
|
+
if (filterPaths.length > 0 && !filterPaths.includes(path)) continue;
|
|
1362
1260
|
|
|
1363
|
-
|
|
1261
|
+
const { db, apis } = confServer[host][path];
|
|
1262
|
+
if (!db || !apis) continue;
|
|
1364
1263
|
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
if (!dbProvider || !dbProvider.models) {
|
|
1369
|
-
logger.error('Failed to load database provider', { host, path });
|
|
1264
|
+
// Check if 'file' api is in the apis list
|
|
1265
|
+
if (!apis.includes('file')) {
|
|
1266
|
+
logger.info('Skipping - no file api in configuration', { host, path });
|
|
1370
1267
|
continue;
|
|
1371
1268
|
}
|
|
1372
1269
|
|
|
1373
|
-
|
|
1270
|
+
// logger.info('Processing host+path with file api', { host, path, db: db.name });
|
|
1271
|
+
|
|
1272
|
+
try {
|
|
1273
|
+
// Connect to database with retry
|
|
1274
|
+
let dbProvider;
|
|
1275
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
1276
|
+
try {
|
|
1277
|
+
dbProvider = await DataBaseProvider.load({ apis, host, path, db });
|
|
1278
|
+
break;
|
|
1279
|
+
} catch (err) {
|
|
1280
|
+
if (attempt === 3) throw err;
|
|
1281
|
+
logger.warn('Database connection failed, retrying...', { attempt, host, path, error: err.message });
|
|
1282
|
+
await timer(3000);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
if (!dbProvider || !dbProvider.models) {
|
|
1286
|
+
logger.error('Failed to load database provider', { host, path });
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1374
1289
|
|
|
1375
|
-
|
|
1376
|
-
connectionsToClose.push({ host, path, dbProvider });
|
|
1290
|
+
const { models } = dbProvider;
|
|
1377
1291
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
logger.warn('File model not loaded', { host, path });
|
|
1381
|
-
continue;
|
|
1382
|
-
}
|
|
1292
|
+
// Track this connection for cleanup
|
|
1293
|
+
connectionsToClose.push({ host, path, dbProvider });
|
|
1383
1294
|
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1295
|
+
// Check if File model exists
|
|
1296
|
+
if (!models.File) {
|
|
1297
|
+
logger.warn('File model not loaded', { host, path });
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1387
1300
|
|
|
1388
|
-
|
|
1301
|
+
// Get all File documents
|
|
1302
|
+
const allFiles = await models.File.find({}, '_id').lean();
|
|
1303
|
+
logger.info('Found File documents', { count: allFiles.length, host, path });
|
|
1389
1304
|
|
|
1390
|
-
|
|
1391
|
-
const referencedFileIds = new Set();
|
|
1305
|
+
if (allFiles.length === 0) continue;
|
|
1392
1306
|
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
const { api, model: modelFields } = refConfig;
|
|
1307
|
+
// Track which File IDs are referenced
|
|
1308
|
+
const referencedFileIds = new Set();
|
|
1396
1309
|
|
|
1397
|
-
// Check
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1401
|
-
.join('');
|
|
1402
|
-
const Model = models[modelName];
|
|
1310
|
+
// Check each API from file.ref.json
|
|
1311
|
+
for (const refConfig of fileRefData) {
|
|
1312
|
+
const { api, model: modelFields } = refConfig;
|
|
1403
1313
|
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1314
|
+
// Check if this API is loaded in current context
|
|
1315
|
+
const modelName = api
|
|
1316
|
+
.split('-')
|
|
1317
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1318
|
+
.join('');
|
|
1319
|
+
const Model = models[modelName];
|
|
1408
1320
|
|
|
1409
|
-
|
|
1321
|
+
if (!Model) {
|
|
1322
|
+
logger.debug('Model not loaded in current context', { api, modelName, host, path });
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1410
1325
|
|
|
1411
|
-
|
|
1412
|
-
const checkFieldReferences = async (fieldPath, fieldConfig) => {
|
|
1413
|
-
for (const [fieldName, fieldValue] of Object.entries(fieldConfig)) {
|
|
1414
|
-
const currentPath = fieldPath ? `${fieldPath}.${fieldName}` : fieldName;
|
|
1326
|
+
logger.info('Checking references in model', { api, modelName });
|
|
1415
1327
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1328
|
+
// Helper function to recursively check field references
|
|
1329
|
+
const checkFieldReferences = async (fieldPath, fieldConfig) => {
|
|
1330
|
+
for (const [fieldName, fieldValue] of Object.entries(fieldConfig)) {
|
|
1331
|
+
const currentPath = fieldPath ? `${fieldPath}.${fieldName}` : fieldName;
|
|
1420
1332
|
|
|
1421
|
-
|
|
1333
|
+
if (fieldValue === true) {
|
|
1334
|
+
// This is a File reference field
|
|
1335
|
+
const query = {};
|
|
1336
|
+
query[currentPath] = { $exists: true, $ne: null };
|
|
1422
1337
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
const
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
value =
|
|
1429
|
-
|
|
1338
|
+
const docs = await Model.find(query, currentPath).lean();
|
|
1339
|
+
|
|
1340
|
+
for (const doc of docs) {
|
|
1341
|
+
// Navigate to the nested field
|
|
1342
|
+
const parts = currentPath.split('.');
|
|
1343
|
+
let value = doc;
|
|
1344
|
+
for (const part of parts) {
|
|
1345
|
+
value = value?.[part];
|
|
1346
|
+
}
|
|
1430
1347
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1348
|
+
if (value) {
|
|
1349
|
+
if (Array.isArray(value)) {
|
|
1350
|
+
value.forEach((id) => id && referencedFileIds.add(id.toString()));
|
|
1351
|
+
} else {
|
|
1352
|
+
referencedFileIds.add(value.toString());
|
|
1353
|
+
}
|
|
1436
1354
|
}
|
|
1437
1355
|
}
|
|
1438
|
-
}
|
|
1439
1356
|
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1357
|
+
logger.info('Found references', {
|
|
1358
|
+
model: modelName,
|
|
1359
|
+
field: currentPath,
|
|
1360
|
+
count: docs.length,
|
|
1361
|
+
});
|
|
1362
|
+
} else if (typeof fieldValue === 'object') {
|
|
1363
|
+
// Nested object, recurse
|
|
1364
|
+
await checkFieldReferences(currentPath, fieldValue);
|
|
1365
|
+
}
|
|
1448
1366
|
}
|
|
1449
|
-
}
|
|
1450
|
-
};
|
|
1367
|
+
};
|
|
1451
1368
|
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
logger.info('Total referenced File IDs', { count: referencedFileIds.size, host, path });
|
|
1369
|
+
await checkFieldReferences('', modelFields);
|
|
1370
|
+
}
|
|
1456
1371
|
|
|
1457
|
-
|
|
1458
|
-
const orphanedFiles = allFiles.filter((file) => !referencedFileIds.has(file._id.toString()));
|
|
1372
|
+
logger.info('Total referenced File IDs', { count: referencedFileIds.size, host, path });
|
|
1459
1373
|
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
} else {
|
|
1463
|
-
logger.info('Found orphaned files', { count: orphanedFiles.length, host, path });
|
|
1374
|
+
// Find orphaned files
|
|
1375
|
+
const orphanedFiles = allFiles.filter((file) => !referencedFileIds.has(file._id.toString()));
|
|
1464
1376
|
|
|
1465
|
-
if (
|
|
1466
|
-
logger.info('
|
|
1467
|
-
count: orphanedFiles.length,
|
|
1468
|
-
ids: orphanedFiles.map((f) => f._id.toString()),
|
|
1469
|
-
});
|
|
1377
|
+
if (orphanedFiles.length === 0) {
|
|
1378
|
+
logger.info('No orphaned files found', { host, path });
|
|
1470
1379
|
} else {
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1380
|
+
logger.info('Found orphaned files', { count: orphanedFiles.length, host, path });
|
|
1381
|
+
|
|
1382
|
+
if (options.dryRun) {
|
|
1383
|
+
logger.info('Dry run - would delete files', {
|
|
1384
|
+
count: orphanedFiles.length,
|
|
1385
|
+
ids: orphanedFiles.map((f) => f._id.toString()),
|
|
1386
|
+
});
|
|
1387
|
+
} else {
|
|
1388
|
+
const orphanedIds = orphanedFiles.map((f) => f._id);
|
|
1389
|
+
const deleteResult = await models.File.deleteMany({ _id: { $in: orphanedIds } });
|
|
1390
|
+
logger.info('Deleted orphaned files', {
|
|
1391
|
+
deletedCount: deleteResult.deletedCount,
|
|
1392
|
+
host,
|
|
1393
|
+
path,
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1478
1396
|
}
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
logger.error('Error processing host+path', {
|
|
1399
|
+
host,
|
|
1400
|
+
path,
|
|
1401
|
+
error: error.message,
|
|
1402
|
+
});
|
|
1479
1403
|
}
|
|
1480
|
-
} catch (error) {
|
|
1481
|
-
logger.error('Error processing host+path', {
|
|
1482
|
-
host,
|
|
1483
|
-
path,
|
|
1484
|
-
error: error.message,
|
|
1485
|
-
});
|
|
1486
1404
|
}
|
|
1487
1405
|
}
|
|
1488
1406
|
}
|
|
1489
|
-
}
|
|
1490
1407
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1408
|
+
// Close all connections
|
|
1409
|
+
logger.info('Closing all database connections', { count: connectionsToClose.length });
|
|
1410
|
+
for (const { host, path, dbProvider } of connectionsToClose) {
|
|
1411
|
+
try {
|
|
1412
|
+
if (dbProvider && dbProvider.close) {
|
|
1413
|
+
await dbProvider.close();
|
|
1414
|
+
logger.info('Connection closed', { host, path });
|
|
1415
|
+
}
|
|
1416
|
+
} catch (error) {
|
|
1417
|
+
logger.error('Error closing connection', { host, path, error: error.message });
|
|
1498
1418
|
}
|
|
1499
|
-
} catch (error) {
|
|
1500
|
-
logger.error('Error closing connection', { host, path, error: error.message });
|
|
1501
1419
|
}
|
|
1502
|
-
}
|
|
1503
1420
|
|
|
1504
|
-
|
|
1421
|
+
logger.info('File collection cleanup completed');
|
|
1422
|
+
} catch (error) {
|
|
1423
|
+
logger.error('File collection cleanup failed', { error: error.message });
|
|
1424
|
+
throw error;
|
|
1425
|
+
}
|
|
1505
1426
|
},
|
|
1506
1427
|
|
|
1507
1428
|
/**
|
|
@@ -1520,6 +1441,7 @@ class UnderpostDB {
|
|
|
1520
1441
|
* @param {boolean} [options.export=false] - Export metadata to backup.
|
|
1521
1442
|
* @param {boolean} [options.instances=false] - Process instances collection.
|
|
1522
1443
|
* @param {boolean} [options.crons=false] - Process crons collection.
|
|
1444
|
+
* @param {boolean} [options.dev=false] - Development mode flag.
|
|
1523
1445
|
* @return {Promise<void>} Resolves when backup operation is complete.
|
|
1524
1446
|
*/
|
|
1525
1447
|
async clusterMetadataBackupCallback(
|
|
@@ -1533,70 +1455,76 @@ class UnderpostDB {
|
|
|
1533
1455
|
export: false,
|
|
1534
1456
|
instances: false,
|
|
1535
1457
|
crons: false,
|
|
1458
|
+
dev: false,
|
|
1536
1459
|
},
|
|
1537
1460
|
) {
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
if (options.generate === true) {
|
|
1551
|
-
logger.info('Generating cluster metadata');
|
|
1552
|
-
await Underpost.db.clusterMetadataFactory(deployId, host, path);
|
|
1553
|
-
}
|
|
1461
|
+
try {
|
|
1462
|
+
loadCronDeployEnv();
|
|
1463
|
+
deployId = deployId ? deployId : process.env.DEFAULT_DEPLOY_ID;
|
|
1464
|
+
host = host ? host : process.env.DEFAULT_DEPLOY_HOST;
|
|
1465
|
+
path = path ? path : process.env.DEFAULT_DEPLOY_PATH;
|
|
1466
|
+
|
|
1467
|
+
logger.info('Starting cluster metadata backup operation', {
|
|
1468
|
+
deployId,
|
|
1469
|
+
host,
|
|
1470
|
+
path,
|
|
1471
|
+
options,
|
|
1472
|
+
});
|
|
1554
1473
|
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
fs.mkdirSync(outputPath, { recursive: true });
|
|
1474
|
+
if (options.generate === true) {
|
|
1475
|
+
logger.info('Generating cluster metadata');
|
|
1476
|
+
await Underpost.db.clusterMetadataFactory(deployId, host, path);
|
|
1559
1477
|
}
|
|
1560
|
-
const collection = 'instances';
|
|
1561
1478
|
|
|
1562
|
-
if (options.
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1479
|
+
if (options.instances === true) {
|
|
1480
|
+
const outputPath = './engine-private/instances';
|
|
1481
|
+
if (!fs.existsSync(outputPath)) {
|
|
1482
|
+
fs.mkdirSync(outputPath, { recursive: true });
|
|
1483
|
+
}
|
|
1484
|
+
const collection = 'instances';
|
|
1568
1485
|
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
}
|
|
1486
|
+
if (options.export === true) {
|
|
1487
|
+
logger.info('Exporting instances collection', { outputPath });
|
|
1488
|
+
shellExec(
|
|
1489
|
+
`node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1576
1492
|
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1493
|
+
if (options.import === true) {
|
|
1494
|
+
logger.info('Importing instances collection', { outputPath });
|
|
1495
|
+
shellExec(
|
|
1496
|
+
`node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1581
1499
|
}
|
|
1582
|
-
const collection = 'crons';
|
|
1583
1500
|
|
|
1584
|
-
if (options.
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1501
|
+
if (options.crons === true) {
|
|
1502
|
+
const outputPath = './engine-private/crons';
|
|
1503
|
+
if (!fs.existsSync(outputPath)) {
|
|
1504
|
+
fs.mkdirSync(outputPath, { recursive: true });
|
|
1505
|
+
}
|
|
1506
|
+
const collection = 'crons';
|
|
1507
|
+
|
|
1508
|
+
if (options.export === true) {
|
|
1509
|
+
logger.info('Exporting crons collection', { outputPath });
|
|
1510
|
+
shellExec(
|
|
1511
|
+
`node bin db --export --primary-pod --collections ${collection} --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1590
1514
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1515
|
+
if (options.import === true) {
|
|
1516
|
+
logger.info('Importing crons collection', { outputPath });
|
|
1517
|
+
shellExec(
|
|
1518
|
+
`node bin db --import --primary-pod --drop --preserveUUID --out-path ${outputPath} --hosts ${host} --paths '${path}' ${deployId}`,
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1596
1521
|
}
|
|
1597
|
-
}
|
|
1598
1522
|
|
|
1599
|
-
|
|
1523
|
+
logger.info('Cluster metadata backup operation completed');
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
logger.error('Cluster metadata backup operation failed', { error: error.message });
|
|
1526
|
+
throw error;
|
|
1527
|
+
}
|
|
1600
1528
|
},
|
|
1601
1529
|
};
|
|
1602
1530
|
}
|