@underpostnet/cyberia 3.3.0 → 3.3.76
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/.dockerignore +1 -0
- package/.env.example +17 -5
- package/.github/workflows/coverall.cyberia.ci.yml +39 -14
- package/.github/workflows/cyberia-client.cd.yml +5 -0
- package/.github/workflows/cyberia-server.cd.yml +5 -0
- package/.github/workflows/docker-image.cyberia-client.ci.yml +8 -5
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +8 -3
- package/.github/workflows/docker-image.cyberia-server.ci.yml +8 -5
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +8 -5
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +15 -1
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +15 -1
- package/.github/workflows/engine-cyberia.cd.yml +3 -0
- package/.github/workflows/engine-cyberia.ci.yml +4 -5
- package/.github/workflows/ghpkg.ci.yml +35 -20
- package/.github/workflows/hardhat.ci.yml +12 -11
- package/.github/workflows/npmpkg.ci.yml +17 -15
- package/.github/workflows/publish.cyberia.ci.yml +44 -0
- package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
- package/.github/workflows/release.cd.yml +1 -0
- package/.prettierignore +1 -0
- package/AGENTS.md +96 -0
- package/CHANGELOG.md +653 -2513
- package/CLI-HELP.md +323 -157
- package/Dockerfile +25 -9
- package/Dockerfile.dev +25 -9
- package/Dockerfile.test +5 -5
- package/README.md +48 -28
- package/bin/build.js +86 -41
- package/bin/build.template.js +8 -3
- package/bin/cyberia.js +1494 -1403
- package/bin/deploy.js +18 -104
- package/bin/index.js +1494 -1403
- package/compose.env +17 -5
- package/conf.js +98 -7
- package/deploy/cyberia-client/deploy.sh +46 -0
- package/deploy/cyberia-client/package.sh +17 -0
- package/deploy/cyberia-client/state.sh +15 -0
- package/deploy/cyberia-server/deploy.sh +46 -0
- package/deploy/cyberia-server/package.sh +17 -0
- package/deploy/cyberia-server/state.sh +15 -0
- package/deploy/dd-cyberia/deploy.sh +9 -29
- package/deploy/dd-cyberia/init.sh +38 -45
- package/deploy/dd-cyberia/package.sh +16 -0
- package/deploy/dd-cyberia/state.sh +14 -0
- package/deploy/dd-cyberia/sync-deploy.sh +133 -147
- package/deploy/lib/config.sh +29 -0
- package/deploy/lib/github-actions-logging.sh +610 -0
- package/deploy/lib/host.sh +194 -0
- package/deploy/lib/state.sh +65 -0
- package/deploy/pwa-microservices-template/deploy.sh +17 -42
- package/deploy/release/deploy.sh +9 -41
- package/deployment.yaml +7 -2
- package/docker-compose.yml +23 -17
- package/examples/static-page/README.md +19 -19
- package/examples/static-page/ssr-components/CustomPage.js +2 -2
- package/hardhat/hardhat.config.js +1 -6
- package/hardhat/package-lock.json +101 -40
- package/hardhat/package.json +2 -2
- package/manifests/alertmanager/deployment.yaml +81 -0
- package/manifests/blackbox-exporter/deployment.yaml +75 -0
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +32 -7
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +32 -7
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/mongo-express/deployment.yaml +46 -27
- package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
- package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
- package/manifests/deployment/mongo-express/service.yaml +16 -0
- package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
- package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
- package/manifests/grafana/deployment.yaml +26 -7
- package/manifests/grafana/kustomization.yaml +1 -0
- package/manifests/grafana/service.yaml +6 -1
- package/manifests/mariadb/pv.yaml +1 -0
- package/manifests/mongodb/statefulset.yaml +9 -2
- package/manifests/mysql/pv-pvc.yaml +14 -1
- package/manifests/postgresql/pv.yaml +12 -1
- package/manifests/postgresql/pvc.yaml +1 -0
- package/manifests/prometheus/deployment.yaml +18 -22
- package/package.json +87 -12
- package/scripts/event-monitor.sh +56 -0
- package/scripts/k3s-node-setup.sh +27 -14
- package/scripts/kubeadm-node-setup.sh +31 -11
- package/scripts/maas-setup.sh +8 -8
- package/scripts/selinux-normalize.sh +907 -0
- package/scripts/shutdown-machine.sh +843 -0
- package/scripts/test-monitor.sh +5 -5
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +15 -10
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +7 -2
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +90 -186
- package/src/api/core/core.controller.js +1 -1
- package/src/api/core/core.router.js +2 -2
- package/src/api/core/core.service.js +2 -2
- package/src/api/crypto/crypto.controller.js +1 -1
- package/src/api/crypto/crypto.service.js +1 -1
- package/src/api/cyberia-action/cyberia-action.controller.js +1 -1
- package/src/api/cyberia-action/cyberia-action.router.js +3 -3
- package/src/api/cyberia-action/cyberia-action.service.js +5 -6
- package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
- package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
- package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
- package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
- package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +2 -4
- package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
- package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +13 -35
- package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +17 -34
- package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
- package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +2 -2
- package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +2 -2
- package/src/api/cyberia-entity/cyberia-entity.controller.js +1 -1
- package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
- package/src/api/cyberia-entity/cyberia-entity.router.js +1 -1
- package/src/api/cyberia-entity/cyberia-entity.service.js +3 -3
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +17 -2
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +468 -5
- package/src/api/cyberia-instance/cyberia-fallback-capture.js +502 -0
- package/src/api/cyberia-instance/cyberia-fallback-world.js +29 -40
- package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
- package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
- package/src/api/cyberia-instance/cyberia-instance-map.service.js +35 -142
- package/src/api/cyberia-instance/cyberia-instance.controller.js +2 -5
- package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
- package/src/api/cyberia-instance/cyberia-instance.router.js +5 -18
- package/src/api/cyberia-instance/cyberia-instance.service.js +5 -45
- package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +76 -3
- package/src/api/cyberia-map/cyberia-map.controller.js +1 -1
- package/src/api/cyberia-map/cyberia-map.router.js +1 -1
- package/src/api/cyberia-map/cyberia-map.service.js +8 -4
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
- package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
- package/src/api/cyberia-quest/cyberia-quest.router.js +4 -4
- package/src/api/cyberia-quest/cyberia-quest.service.js +5 -5
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +1 -1
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +1 -1
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +2 -2
- package/src/api/cyberia-saga/cyberia-saga.controller.js +1 -1
- package/src/api/cyberia-saga/cyberia-saga.router.js +1 -1
- package/src/api/cyberia-saga/cyberia-saga.service.js +2 -2
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -214
- package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
- package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
- package/src/api/cyberia-skill/cyberia-skill.router.js +1 -1
- package/src/api/cyberia-skill/cyberia-skill.service.js +2 -2
- package/src/api/default/default.controller.js +1 -1
- package/src/api/default/default.router.js +1 -1
- package/src/api/default/default.service.js +2 -2
- package/src/api/document/document.controller.js +1 -1
- package/src/api/document/document.router.js +1 -1
- package/src/api/document/document.service.js +2 -2
- package/src/api/file/file.controller.js +1 -1
- package/src/api/file/file.ref.js +153 -0
- package/src/api/file/file.ref.json +11 -2
- package/src/api/file/file.router.js +1 -1
- package/src/api/file/file.service.js +2 -2
- package/src/api/instance/instance.controller.js +1 -1
- package/src/api/instance/instance.router.js +1 -1
- package/src/api/instance/instance.service.js +2 -2
- package/src/api/ipfs/ipfs.controller.js +1 -1
- package/src/api/ipfs/ipfs.router.js +2 -2
- package/src/api/ipfs/ipfs.service.js +2 -2
- package/src/api/object-layer/object-layer.controller.js +1 -1
- package/src/api/object-layer/object-layer.model.js +47 -12
- package/src/api/object-layer/object-layer.router.js +12 -5
- package/src/api/object-layer/object-layer.service.js +116 -123
- package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +1 -1
- package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +1 -1
- package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +2 -2
- package/src/api/test/test.controller.js +1 -1
- package/src/api/test/test.service.js +1 -1
- package/src/api/user/guest.service.js +3 -3
- package/src/api/user/user.controller.js +1 -1
- package/src/api/user/user.router.js +2 -2
- package/src/api/user/user.service.js +4 -4
- package/src/api.js +5 -9
- package/src/cli/app.js +402 -0
- package/src/cli/baremetal.js +30 -35
- package/src/cli/client.js +306 -0
- package/src/cli/cloud-init.js +3 -3
- package/src/cli/cluster.js +191 -82
- package/src/cli/db.js +120 -68
- package/src/cli/deploy.js +157 -75
- package/src/cli/docker-compose.js +5 -5
- package/src/cli/domains.js +184 -0
- package/src/cli/dotenv-store.js +143 -0
- package/src/cli/event.js +2281 -0
- package/src/cli/fs-selection.js +115 -0
- package/src/cli/fs.js +159 -343
- package/src/cli/host.js +672 -0
- package/src/cli/image.js +4 -6
- package/src/cli/index.js +343 -151
- package/src/cli/ipfs.js +34 -30
- package/src/cli/kickstart.js +1 -1
- package/src/cli/kubectl.js +218 -41
- package/src/cli/lxd.js +3 -3
- package/src/cli/monitor.js +1359 -26
- package/src/cli/package.js +90 -0
- package/src/cli/release.js +16 -12
- package/src/cli/repository.js +586 -363
- package/src/cli/run.js +398 -843
- package/src/cli/secrets.js +2158 -964
- package/src/cli/ssh.js +478 -279
- package/src/cli/state.js +394 -0
- package/src/cli/static.js +2 -2
- package/src/cli/system.js +1 -1
- package/src/cli/test.js +203 -125
- package/src/cli/vultr.js +61 -21
- package/src/cli/wireguard.js +1112 -306
- package/src/client/components/core/CalendarCore.js +53 -96
- package/src/client/components/core/Docs.js +327 -303
- package/src/client/components/core/FileExplorer.js +0 -21
- package/src/client/components/core/Modal.js +56 -1
- package/src/client/components/core/PanelForm.js +4 -4
- package/src/client/components/core/Repository.js +56 -0
- package/src/client/components/core/Translate.js +7 -0
- package/src/client/components/core/{FullScreen.js → ViewModeController.js} +42 -39
- package/src/client/components/core/Wallet.js +0 -11
- package/src/client/components/core/Worker.js +7 -1
- package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +2 -2
- package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
- package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
- package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
- package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
- package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
- package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
- package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
- package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +2 -35
- package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
- package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
- package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
- package/src/client/components/default/AppShellDefault.js +1 -1
- package/src/client/components/default/SettingsDefault.js +2 -2
- package/src/client/components/itemledger/SettingsItemledger.js +2 -2
- package/src/client/components/underpost/AppShellUnderpost.js +3 -2
- package/src/client/components/underpost/SettingsUnderpost.js +2 -2
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +313 -30
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
- package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
- package/src/client/public/cyberia-docs/CYBERIA.md +48 -28
- package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
- package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +23 -6
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
- package/src/client/public/default/android-chrome-144x144.png +0 -0
- package/src/client/public/default/android-chrome-192x192.png +0 -0
- package/src/client/public/default/android-chrome-256x256.png +0 -0
- package/src/client/public/default/android-chrome-36x36.png +0 -0
- package/src/client/public/default/android-chrome-384x384.png +0 -0
- package/src/client/public/default/android-chrome-48x48.png +0 -0
- package/src/client/public/default/android-chrome-512x512.png +0 -0
- package/src/client/public/default/android-chrome-72x72.png +0 -0
- package/src/client/public/default/android-chrome-96x96.png +0 -0
- package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
- package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
- package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
- package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
- package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
- package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
- package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
- package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
- package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
- package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
- package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
- package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
- package/src/client/public/default/apple-touch-icon.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
- package/src/client/public/default/assets/background/white.jpg +0 -0
- package/src/client/public/default/browserconfig.xml +1 -1
- package/src/client/public/default/favicon-16x16.png +0 -0
- package/src/client/public/default/favicon-32x32.png +0 -0
- package/src/client/public/default/favicon-48x48.png +0 -0
- package/src/client/public/default/favicon.ico +0 -0
- package/src/client/public/default/manifest.webmanifest +5 -5
- package/src/client/public/default/mstile-144x144.png +0 -0
- package/src/client/public/default/mstile-150x150.png +0 -0
- package/src/client/public/default/mstile-310x150.png +0 -0
- package/src/client/public/default/mstile-310x310.png +0 -0
- package/src/client/public/default/mstile-70x70.png +0 -0
- package/src/client/public/default/yandex-browser-50x50.png +0 -0
- package/src/client/public/default/yandex-browser-manifest.json +1 -1
- package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
- package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
- package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
- package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
- package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
- package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
- package/src/client/services/object-layer/object-layer.management.js +6 -10
- package/src/client/services/object-layer/object-layer.service.js +3 -1
- package/src/client/ssr/RootDocument.js +1 -1
- package/src/client/ssr/body/404.js +1 -1
- package/src/client/ssr/body/500.js +1 -1
- package/src/client/ssr/body/CacheControl.js +1 -1
- package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +1 -1
- package/src/client/ssr/body/DefaultSplashScreen.js +1 -1
- package/src/client/ssr/body/SwaggerDarkMode.js +1 -1
- package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +1 -1
- package/src/client/ssr/head/CryptokoynScripts.js +1 -1
- package/src/client/ssr/head/Css.js +1 -1
- package/src/client/ssr/head/CyberiaPortalScripts.js +1 -1
- package/src/client/ssr/head/DefaultScripts.js +1 -1
- package/src/client/ssr/head/ItemledgerScripts.js +1 -1
- package/src/client/ssr/head/Microdata.js +1 -1
- package/src/client/ssr/head/Production.js +1 -1
- package/src/client/ssr/head/Pwa.js +1 -1
- package/src/client/ssr/head/PwaItemledger.js +1 -1
- package/src/client/ssr/head/Seo.js +1 -1
- package/src/client/ssr/head/UnderpostScripts.js +1 -1
- package/src/client/ssr/mailer/DefaultRecoverEmail.js +1 -1
- package/src/client/ssr/mailer/DefaultVerifyEmail.js +1 -1
- package/src/client/ssr/views/Cyberia404.js +7 -5
- package/src/client/ssr/views/CyberiaServerMetrics.js +1 -1
- package/src/client/ssr/views/Maintenance.js +1 -1
- package/src/client/ssr/views/NoNetworkConnection.js +1 -1
- package/src/client/ssr/views/Test.js +1 -1
- package/src/client/sw/core.sw.js +20 -14
- package/src/client-builder/client-build-docs.js +92 -77
- package/src/client-builder/client-build-live.js +2 -2
- package/src/client-builder/client-build.js +105 -74
- package/src/client-builder/client-bundle.js +347 -0
- package/src/client-builder/client-dev-server.js +3 -3
- package/src/client-builder/client-icons.js +2 -2
- package/src/client-builder/ssr.js +3 -3
- package/src/client.build.js +3 -6
- package/src/client.dev.js +3 -6
- package/src/db/DataBaseProvider.js +186 -3
- package/src/db/mariadb/MariaDB.js +3 -2
- package/src/db/mongo/MongoBootstrap.js +273 -101
- package/src/db/mongo/MongoExpress.js +187 -0
- package/src/db/mongo/MongooseDB.js +7 -8
- package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
- package/src/grpc/cyberia/grpc-server.js +6 -8
- package/src/index.js +79 -23
- package/src/mailer/EmailRender.js +2 -2
- package/src/mailer/MailerInterceptor.js +87 -0
- package/src/mailer/MailerProvider.js +50 -6
- package/src/projects/cyberia/atlas-sprite-sheet-generator.js +144 -147
- package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
- package/src/projects/cyberia/besu-genesis-generator.js +8 -17
- package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
- package/src/projects/cyberia/catalog-cyberia.js +54 -10
- package/src/projects/cyberia/gemini-client.js +4 -4
- package/src/projects/cyberia/generate-saga.js +5 -12
- package/src/projects/cyberia/hot-reload-trigger.js +18 -29
- package/src/projects/cyberia/instance-backup.js +210 -0
- package/src/projects/cyberia/instance-data.js +178 -258
- package/src/projects/cyberia/ipfs-client.js +4 -4
- package/src/projects/cyberia/map-preview-generator.js +33 -69
- package/src/projects/cyberia/object-layer.js +153 -320
- package/src/projects/cyberia/seed-audio.js +139 -0
- package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
- package/src/projects/cyberia/semantic-layer-generator.js +3 -19
- package/src/projects/cyberia/stat-balance.js +81 -0
- package/src/projects/cyberia/stat-commands.js +23 -0
- package/src/projects/cyberia/stat-contract-generator.js +101 -0
- package/src/projects/underpost/catalog-underpost.js +9 -1
- package/src/proxy.js +4 -7
- package/src/runtime/cyberia-client/Dockerfile +17 -8
- package/src/runtime/cyberia-client/Dockerfile.dev +13 -4
- package/src/runtime/cyberia-server/Dockerfile +18 -6
- package/src/runtime/cyberia-server/Dockerfile.dev +16 -3
- package/src/runtime/engine-cyberia/Dockerfile +25 -9
- package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
- package/src/runtime/engine-cyberia/Dockerfile.test +5 -5
- package/src/runtime/engine-cyberia/compose.env +17 -5
- package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
- package/src/runtime/express/Express.js +32 -27
- package/src/runtime/lampp/Lampp.js +22 -70
- package/src/runtime/nginx/Nginx.js +1 -1
- package/src/runtime/wp/Wp.js +16 -48
- package/src/server/{catalog.js → build/catalog.js} +15 -5
- package/src/server/build/coverage.js +188 -0
- package/src/server/build/execution.js +402 -0
- package/src/server/build/package.js +508 -0
- package/src/server/build/testing.js +578 -0
- package/src/server/{dns.js → network/dns.js} +144 -11
- package/src/server/{forward-proxy.js → network/forward-proxy.js} +10 -18
- package/src/server/{middlewares.js → network/middlewares.js} +11 -5
- package/src/server/network/node-capability.js +98 -0
- package/src/server/{peer.js → network/peer.js} +3 -3
- package/src/server/{proxy.js → network/proxy.js} +7 -13
- package/src/server/network/router.js +356 -0
- package/src/server/{tls.js → network/tls.js} +2 -2
- package/src/server/{underpost-compression.js → network/underpost-compression.js} +1 -1
- package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +7 -7
- package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +2 -2
- package/src/server/{cri.js → ops/cri.js} +2 -2
- package/src/server/{cron.js → ops/cron.js} +349 -74
- package/src/server/ops/event-notification.js +284 -0
- package/src/server/{logger.js → ops/logger.js} +143 -23
- package/src/server/ops/monitoring.js +1724 -0
- package/src/server/{systemd.js → ops/systemd.js} +55 -1
- package/src/server/{conf.js → runtime/conf.js} +516 -423
- package/src/server/runtime/config-scope.js +215 -0
- package/src/server/runtime/environment.js +149 -0
- package/src/server/{process.js → runtime/process.js} +153 -12
- package/src/server/{runtime-status.js → runtime/runtime-status.js} +155 -16
- package/src/server/{runtime.js → runtime/runtime.js} +12 -8
- package/src/server/{start.js → runtime/start.js} +128 -39
- package/src/server/{auth.js → security/auth.js} +7 -8
- package/src/server/security/container-storage.js +382 -0
- package/src/server/{crypto.js → security/crypto.js} +1 -1
- package/src/server/{selinux.js → security/selinux.js} +1 -1
- package/src/server/{backup.js → storage/backup.js} +17 -8
- package/src/server/{data-query.js → storage/data-query.js} +17 -1
- package/src/server/storage/downloader.js +121 -0
- package/src/server/storage/repository.js +68 -0
- package/src/server/storage/zip.js +153 -0
- package/src/server.js +4 -7
- package/src/ws/IoInterface.js +1 -1
- package/src/ws/IoServer.js +1 -1
- package/src/ws/core/core.ws.connection.js +1 -1
- package/src/ws/core/core.ws.emit.js +1 -1
- package/src/ws/core/core.ws.server.js +1 -1
- package/src/ws/default/default.ws.connection.js +1 -1
- package/src/ws/default/default.ws.emit.js +1 -1
- package/src/ws/default/default.ws.server.js +1 -1
- package/test/e2e/event-e2e-public-ingress-down.js +45 -0
- package/test/e2e/event-e2e-wireguard-server-down.js +38 -0
- package/test/e2e/event-e2e-wireguard-spoke-down.js +40 -0
- package/test/{api.test.js → integration/app/api.test.js} +33 -14
- package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
- package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
- package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +64 -0
- package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
- package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
- package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +17 -23
- package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
- package/test/integration/app/cyberia/cyberia-load.test.js +466 -0
- package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
- package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
- package/test/integration/app/cyberia/fallback-world-capture.test.js +338 -0
- package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -0
- package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
- package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
- package/test/integration/app/cyberia/seed-audio.test.js +275 -0
- package/test/{shape-generator.test.js → integration/app/cyberia/shape-generator.test.js} +1 -1
- package/test/integration/infra/1-security/config-scope.test.js +305 -0
- package/test/integration/infra/1-security/container-storage.test.js +356 -0
- package/test/integration/infra/1-security/in-pod-cli-surface.test.js +86 -0
- package/test/integration/infra/1-security/secret-onboarding.test.js +930 -0
- package/test/{selinux.test.js → integration/infra/1-security/selinux.test.js} +2 -4
- package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js} +415 -61
- package/test/integration/infra/1-security/systemd-service.test.js +70 -0
- package/test/integration/infra/1-security/underpost-config-secret.test.js +367 -0
- package/test/integration/infra/2-network/dns-firewall.test.js +566 -0
- package/test/integration/infra/2-network/node-capability.test.js +182 -0
- package/test/integration/infra/2-network/wireguard-cli.test.js +1187 -0
- package/test/{wireguard-edge.test.js → integration/infra/2-network/wireguard-edge.test.js} +649 -55
- package/test/{cluster-instances.test.js → integration/infra/3-cluster/cluster-instances.test.js} +11 -15
- package/test/{deploy-node-placement.test.js → integration/infra/3-cluster/deploy-node-placement.test.js} +3 -3
- package/test/integration/infra/3-cluster/docker-compose-stack.test.js +313 -0
- package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
- package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
- package/test/integration/infra/4-ingress/deploy-routes.test.js +379 -0
- package/test/integration/infra/4-ingress/gateway-static-assets.test.js +453 -0
- package/test/{instance-traffic-plan.test.js → integration/infra/4-ingress/instance-traffic-plan.test.js} +10 -6
- package/test/{underpost-gateway.test.js → integration/infra/4-ingress/underpost-gateway.test.js} +6 -6
- package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +4 -4
- package/test/integration/infra/5-observability/cron-jobs.test.js +652 -0
- package/test/{deploy-monitor.test.js → integration/infra/5-observability/deploy-monitor.test.js} +29 -31
- package/test/integration/infra/5-observability/event-notification.test.js +197 -0
- package/test/integration/infra/5-observability/event-remediation.test.js +502 -0
- package/test/integration/infra/5-observability/event-targets.test.js +1174 -0
- package/test/integration/infra/5-observability/monitoring-stack.test.js +902 -0
- package/test/support/shell-harness.js +73 -0
- package/test/unit/build-template.test.js +97 -0
- package/test/unit/build-workflow-offline.test.js +132 -0
- package/test/unit/catalog.test.js +71 -0
- package/test/unit/client-build-docs.test.js +361 -0
- package/test/unit/client-bundle.test.js +313 -0
- package/test/unit/conf-loading.test.js +303 -0
- package/test/unit/conf-resolution.test.js +996 -0
- package/test/unit/coverage-artifact.test.js +221 -0
- package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
- package/test/unit/cyberia/instance-backup.test.js +183 -0
- package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
- package/test/unit/cyberia/publish-workflow.test.js +22 -0
- package/test/unit/cyberia/stat-balance.test.js +69 -0
- package/test/unit/deploy-env-resolution.test.js +38 -0
- package/test/unit/deploy-log-table.test.js +518 -0
- package/test/unit/downloader-integrity.test.js +98 -0
- package/test/unit/execution-profiles.test.js +219 -0
- package/test/unit/file-reference-registry.test.js +141 -0
- package/test/unit/fleet-sync-source.test.js +149 -0
- package/test/unit/fs-storage-paths.test.js +596 -0
- package/test/unit/logger-redaction.test.js +141 -0
- package/test/unit/package.test.js +457 -0
- package/test/unit/prepare-host.test.js +382 -0
- package/test/unit/process-environment.test.js +18 -0
- package/test/unit/propagation-message.test.js +78 -0
- package/test/unit/redeploy-plan.test.js +24 -0
- package/test/unit/release-bump.test.js +474 -0
- package/test/unit/ssh-output-redaction.test.js +28 -0
- package/test/unit/start-options.test.js +46 -0
- package/test/unit/test-tiers.test.js +220 -0
- package/test/unit/zip-archive.test.js +142 -0
- package/vitest.config.js +66 -0
- package/.nycrc +0 -9
- package/deploy/lib/logging.sh +0 -96
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
- package/manifests/mongodb/pv-pvc.yaml +0 -59
- package/scripts/coverall-test-cyberia.sh +0 -24
- package/scripts/coverall-test.sh +0 -24
- package/scripts/link-local-underpost-cli.sh +0 -6
- package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
- package/src/api/user/user.build.js +0 -16
- package/src/cli/env.js +0 -177
- package/src/client/components/core/Blockchain.js +0 -41
- package/src/client/components/core/Blog.js +0 -9
- package/src/client/components/core/KeyboardAvoidance.js +0 -145
- package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
- package/src/client/public/default/assets/background/dark.jpg +0 -0
- package/src/client/public/default/assets/background/white0-min.jpg +0 -0
- package/src/client/public/default/assets/background/white0.jpg +0 -0
- package/src/client/public/doc/favicon.ico +0 -0
- package/src/client/public/doc/sitemap +0 -148
- package/src/server/downloader.js +0 -73
- package/src/server/environment.js +0 -98
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Provides utilities for building, loading, and managing server configurations,
|
|
3
3
|
* deployment contexts, and service configurations (API, Client, WS).
|
|
4
|
-
* @module src/server/conf.js
|
|
4
|
+
* @module src/server/runtime/conf.js
|
|
5
5
|
* @namespace ServerConfBuilder
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -13,21 +13,30 @@ import {
|
|
|
13
13
|
getDirname,
|
|
14
14
|
newInstance,
|
|
15
15
|
orderAbc,
|
|
16
|
-
orderArrayFromAttrInt,
|
|
17
|
-
range,
|
|
18
16
|
timer,
|
|
19
|
-
} from '
|
|
17
|
+
} from '../../client/components/core/CommonJs.js';
|
|
20
18
|
import * as dir from 'path';
|
|
21
19
|
import net from 'net';
|
|
22
20
|
import crypto from 'crypto';
|
|
23
21
|
import colors from 'colors';
|
|
24
|
-
import { loggerFactory } from '
|
|
25
|
-
import { writeEnv } from './environment.js';
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
22
|
+
import { loggerFactory } from '../ops/logger.js';
|
|
23
|
+
import { isOciRuntime, writeEnv } from './environment.js';
|
|
24
|
+
import { configRejectionFactory } from './config-scope.js';
|
|
25
|
+
import { shellArgumentFactory, shellExec } from './process.js';
|
|
26
|
+
import { UNDERPOST_GATEWAY, statusPageAssetPathFactory } from '../network/underpost-gateway.js';
|
|
27
|
+
import { COVERAGE_BUNDLE_DIRECTORY } from '../build/coverage.js';
|
|
28
|
+
import { DefaultConf } from '../../../conf.js';
|
|
29
29
|
import splitFile from 'split-file';
|
|
30
|
-
import
|
|
30
|
+
import { readDeployRoutes } from '../network/router.js';
|
|
31
|
+
import Underpost from '../../index.js';
|
|
32
|
+
import {
|
|
33
|
+
clearAwaitDeploy,
|
|
34
|
+
getRuntimeStatus,
|
|
35
|
+
isAwaitingDeploy,
|
|
36
|
+
latchAwaitDeploy,
|
|
37
|
+
RUNTIME_STATUS,
|
|
38
|
+
} from './runtime-status.js';
|
|
39
|
+
import { cronDeployIdResolve } from '../ops/cron.js';
|
|
31
40
|
|
|
32
41
|
colors.enable();
|
|
33
42
|
|
|
@@ -176,8 +185,8 @@ const getConfFolder = (deployId) => {
|
|
|
176
185
|
|
|
177
186
|
/**
|
|
178
187
|
* Resolves the full path to a specific configuration JSON file for a deploy ID.
|
|
179
|
-
* For `server` configs
|
|
180
|
-
*
|
|
188
|
+
* For `server` configs it prefers the `dev.<subConf>` variant when one exists: always for a
|
|
189
|
+
* sub-conf the caller named, and only in development for one inherited from `DEPLOY_SUB_CONF`.
|
|
181
190
|
*
|
|
182
191
|
* @method getConfFilePath
|
|
183
192
|
* @param {string} deployId - The deploy ID.
|
|
@@ -191,17 +200,28 @@ const getConfFolder = (deployId) => {
|
|
|
191
200
|
* // => './engine-private/conf/dd-myapp/conf.server.json'
|
|
192
201
|
*
|
|
193
202
|
* @example
|
|
194
|
-
* //
|
|
203
|
+
* // With an explicitly named subConf, in any environment:
|
|
195
204
|
* getConfFilePath('dd-myapp', 'server', 'local');
|
|
196
205
|
* // => './engine-private/conf/dd-myapp/conf.server.dev.local.json' (if it exists)
|
|
197
206
|
*/
|
|
198
207
|
const getConfFilePath = (deployId, confType, subConf = '') => {
|
|
199
208
|
const folder = getConfFolder(deployId);
|
|
200
|
-
|
|
201
|
-
|
|
209
|
+
const explicit = `${subConf ?? ''}`.trim();
|
|
210
|
+
// Without an explicit sub-conf, fall back to the env var loadConf() exported.
|
|
211
|
+
const effectiveSubConf = explicit || process.env.DEPLOY_SUB_CONF || '';
|
|
202
212
|
if (confType === 'server' && effectiveSubConf) {
|
|
203
|
-
|
|
204
|
-
|
|
213
|
+
// An explicitly named sub-conf is a request, not a hint: honor it whatever the ambient
|
|
214
|
+
// environment says. Only the env-var fallback stays gated on development, so a production
|
|
215
|
+
// deploy that merely carries DEPLOY_SUB_CONF is unaffected.
|
|
216
|
+
//
|
|
217
|
+
// The gate used to apply to both, and every `underpost` invocation loads the host
|
|
218
|
+
// configuration store with `override: true` — on a provisioned node that store carries
|
|
219
|
+
// NODE_ENV=production, so the gate was false for every CLI call and a named sub-conf was
|
|
220
|
+
// silently discarded in favour of the base conf.
|
|
221
|
+
if (explicit || process.env.NODE_ENV === 'development') {
|
|
222
|
+
const devConfPath = `${folder}/conf.${confType}.dev.${effectiveSubConf}.json`;
|
|
223
|
+
if (fs.existsSync(devConfPath)) return devConfPath;
|
|
224
|
+
}
|
|
205
225
|
}
|
|
206
226
|
return `${folder}/conf.${confType}.json`;
|
|
207
227
|
};
|
|
@@ -261,7 +281,7 @@ const Config = {
|
|
|
261
281
|
deployContext = process.env.DEPLOY_ID;
|
|
262
282
|
if (!subConf && process.argv[3] && typeof process.argv[3] === 'string') subConf = process.argv[3];
|
|
263
283
|
|
|
264
|
-
|
|
284
|
+
latchAwaitDeploy();
|
|
265
285
|
if (deployContext.startsWith('dd-')) loadConf(deployContext, subConf);
|
|
266
286
|
if (deployContext === 'proxy') await Config.buildProxy(deployList, subConf);
|
|
267
287
|
},
|
|
@@ -269,16 +289,15 @@ const Config = {
|
|
|
269
289
|
* @method deployIdFactory
|
|
270
290
|
* @description Creates a new deploy ID.
|
|
271
291
|
* @param {string} [deployId='dd-default']
|
|
272
|
-
* @param {object} [options={ subConf: ''
|
|
292
|
+
* @param {object} [options={ subConf: '' }] - The options.
|
|
273
293
|
* @memberof ServerConfBuilder
|
|
274
294
|
*/
|
|
275
|
-
deployIdFactory: function (deployId = DEFAULT_DEPLOY_ID, options = { subConf: ''
|
|
295
|
+
deployIdFactory: function (deployId = DEFAULT_DEPLOY_ID, options = { subConf: '' }) {
|
|
276
296
|
if (!deployId.startsWith('dd-')) deployId = `dd-${deployId}`;
|
|
277
297
|
|
|
278
298
|
logger.info('Build deployId', deployId);
|
|
279
299
|
|
|
280
300
|
const folder = `./engine-private/conf/${deployId}`;
|
|
281
|
-
const repoName = `engine-${deployId.split('dd-')[1]}`;
|
|
282
301
|
|
|
283
302
|
if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
|
|
284
303
|
|
|
@@ -330,29 +349,6 @@ const Config = {
|
|
|
330
349
|
);
|
|
331
350
|
}
|
|
332
351
|
|
|
333
|
-
if (options.cluster === true) {
|
|
334
|
-
fs.writeFileSync(
|
|
335
|
-
`./.github/workflows/${repoName}.cd.yml`,
|
|
336
|
-
fs.readFileSync(`./.github/workflows/engine-test.cd.yml`, 'utf8').replaceAll('test', deployId.split('dd-')[1]),
|
|
337
|
-
'utf8',
|
|
338
|
-
);
|
|
339
|
-
fs.writeFileSync(
|
|
340
|
-
`./.github/workflows/${repoName}.ci.yml`,
|
|
341
|
-
fs.readFileSync(`./.github/workflows/engine-test.ci.yml`, 'utf8').replaceAll('test', deployId.split('dd-')[1]),
|
|
342
|
-
'utf8',
|
|
343
|
-
);
|
|
344
|
-
shellExec(`node bin new --default-conf --deploy-id ${deployId}`);
|
|
345
|
-
|
|
346
|
-
if (!fs.existsSync(`./engine-private/deploy/dd.router`))
|
|
347
|
-
fs.writeFileSync(`./engine-private/deploy/dd.router`, deployId, 'utf8');
|
|
348
|
-
else
|
|
349
|
-
fs.writeFileSync(
|
|
350
|
-
`./engine-private/deploy/dd.router`,
|
|
351
|
-
fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').trim() + `,${deployId}`,
|
|
352
|
-
'utf8',
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
352
|
return { deployIdFolder: folder, deployId };
|
|
357
353
|
},
|
|
358
354
|
/**
|
|
@@ -412,17 +408,178 @@ const Config = {
|
|
|
412
408
|
* @param {string} [subConf=''] - The sub configuration.
|
|
413
409
|
* @memberof ServerConfBuilder
|
|
414
410
|
*/
|
|
411
|
+
/**
|
|
412
|
+
* @method deployEnvFilePath
|
|
413
|
+
* @description Resolves a deployment's env file for an environment.
|
|
414
|
+
*
|
|
415
|
+
* A named sub-configuration selects `.env.<env>.<subConf>` when that file exists and degrades to
|
|
416
|
+
* the plain `.env.<env>` when it does not. Single source of this precedence: {@link loadConf}
|
|
417
|
+
* materializes the working tree from it and {@link UnderpostApp} addresses it from the CLI, so
|
|
418
|
+
* both always agree on which file is in effect.
|
|
419
|
+
* @param {string} deployId - Deployment id.
|
|
420
|
+
* @param {string} [env='production'] - Environment selector.
|
|
421
|
+
* @param {string} [subConf] - Sub-configuration name.
|
|
422
|
+
* @returns {string} Path to the deployment env file.
|
|
423
|
+
* @memberof ServerConfBuilder
|
|
424
|
+
*/
|
|
425
|
+
const deployEnvFilePath = (deployId, env = 'production', subConf = '') => {
|
|
426
|
+
const base = `${getConfFolder(deployId)}/.env.${env || 'production'}`;
|
|
427
|
+
const name = `${subConf ?? ''}`.trim();
|
|
428
|
+
if (!name) return base;
|
|
429
|
+
const scoped = `${base}.${name}`;
|
|
430
|
+
return fs.existsSync(scoped) ? scoped : base;
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* The environments a deployment's configuration actually carries.
|
|
435
|
+
*
|
|
436
|
+
* Read from the tree rather than assumed, because which environments exist is a property of the
|
|
437
|
+
* checkout: a workstation carries every one a developer runs, and a workload carries none at all —
|
|
438
|
+
* its environment is injected. A name is the segment after `.env.` and nothing further, so an
|
|
439
|
+
* overlay (`.env.production.oci`) and a sub-configuration (`.env.development.api`) are the
|
|
440
|
+
* variants of an environment, never environments of their own.
|
|
441
|
+
* @param {string} deployId - Deployment id.
|
|
442
|
+
* @returns {string[]} Environment names present, sorted.
|
|
443
|
+
* @memberof ServerConfBuilder
|
|
444
|
+
*/
|
|
445
|
+
const deployEnvNamesFactory = (deployId) => {
|
|
446
|
+
const folder = getConfFolder(deployId);
|
|
447
|
+
if (!fs.existsSync(folder)) return [];
|
|
448
|
+
return fs
|
|
449
|
+
.readdirSync(folder)
|
|
450
|
+
.map((entry) => `${entry}`.match(/^\.env\.([^.]+)$/)?.[1])
|
|
451
|
+
.filter(Boolean)
|
|
452
|
+
.sort();
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Resolves a custom instance's env file for an environment.
|
|
457
|
+
*
|
|
458
|
+
* An instance's runtime environment is its own file, not a sub-configuration of the parent
|
|
459
|
+
* deployment's: `instance-build-manifest` writes it, the instance container sources it at boot,
|
|
460
|
+
* and {@link UnderpostApp} addresses it from the CLI through `--args instance-id=`. Single
|
|
461
|
+
* source of that path, so the three can never disagree.
|
|
462
|
+
* @param {string} deployId - Parent deployment id.
|
|
463
|
+
* @param {string} instanceId - Instance id, as declared in `conf.instances.json`.
|
|
464
|
+
* @param {string} [env='production'] - Environment selector.
|
|
465
|
+
* @returns {string} Path to the instance env file.
|
|
466
|
+
* @memberof ServerConfBuilder
|
|
467
|
+
*/
|
|
468
|
+
const instanceEnvFilePath = (deployId, instanceId, env = 'production') =>
|
|
469
|
+
`${getConfFolder(deployId)}/instances/${instanceId}/env/${env || 'production'}.env`;
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Suffix marking a deployment env file's OCI (container) runtime overlay.
|
|
473
|
+
* @constant {string}
|
|
474
|
+
* @memberof ServerConfBuilder
|
|
475
|
+
*/
|
|
476
|
+
const OCI_ENV_SUFFIX = '.oci';
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* @method deployOciEnvFilePath
|
|
480
|
+
* @description Resolves a deployment's OCI runtime env overlay for an environment.
|
|
481
|
+
*
|
|
482
|
+
* The overlay is per environment, not per sub-configuration: it carries the values that only
|
|
483
|
+
* hold when the deployment runs as a container image (cluster-internal service names, in-cluster
|
|
484
|
+
* database endpoints), which are the same whichever host sub-configuration is in effect.
|
|
485
|
+
* @param {string} deployId - Deployment id.
|
|
486
|
+
* @param {string} [env='production'] - Environment selector.
|
|
487
|
+
* @returns {string} Path to the deployment's OCI env overlay.
|
|
488
|
+
* @memberof ServerConfBuilder
|
|
489
|
+
*/
|
|
490
|
+
const deployOciEnvFilePath = (deployId, env = 'production') =>
|
|
491
|
+
`${getConfFolder(deployId)}/.env.${env || 'production'}${OCI_ENV_SUFFIX}`;
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* @method envLineKey
|
|
495
|
+
* @description The variable name a dotenv line declares, or `''` for blanks and comments.
|
|
496
|
+
* @param {string} line - One dotenv line.
|
|
497
|
+
* @returns {string} Declared key, or `''`.
|
|
498
|
+
* @memberof ServerConfBuilder
|
|
499
|
+
*/
|
|
500
|
+
const envLineKey = (line) => {
|
|
501
|
+
const trimmed = `${line}`.trimStart();
|
|
502
|
+
if (!trimmed || trimmed.startsWith('#')) return '';
|
|
503
|
+
const assign = trimmed.indexOf('=');
|
|
504
|
+
return assign === -1 ? '' : trimmed.slice(0, assign).trim();
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* @method ociEnvContentFactory
|
|
509
|
+
* @description Applies an OCI overlay onto base env contents.
|
|
510
|
+
*
|
|
511
|
+
* Textual rather than parse-and-render: base lines the overlay does not redeclare survive
|
|
512
|
+
* verbatim — comments, ordering, and value quoting included — and the overlay is appended whole,
|
|
513
|
+
* so an overridden key appears exactly once with the container value.
|
|
514
|
+
* @param {string} baseContent - The deployment's `.env.<env>` contents.
|
|
515
|
+
* @param {string} overlayContent - The matching `.env.<env>.oci` contents.
|
|
516
|
+
* @returns {string} Overlay-applied env contents.
|
|
517
|
+
* @memberof ServerConfBuilder
|
|
518
|
+
*/
|
|
519
|
+
const ociEnvContentFactory = (baseContent, overlayContent) => {
|
|
520
|
+
const overlay = `${overlayContent ?? ''}`.trim();
|
|
521
|
+
if (!overlay) return baseContent;
|
|
522
|
+
const overridden = new Set(overlay.split('\n').map(envLineKey).filter(Boolean));
|
|
523
|
+
const kept = `${baseContent ?? ''}`
|
|
524
|
+
.split('\n')
|
|
525
|
+
.filter((line) => {
|
|
526
|
+
const key = envLineKey(line);
|
|
527
|
+
return !key || !overridden.has(key);
|
|
528
|
+
})
|
|
529
|
+
.join('\n')
|
|
530
|
+
.trimEnd();
|
|
531
|
+
return `${kept}${kept ? '\n' : ''}${overlay}\n`;
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* @method deployEnvContentFactory
|
|
536
|
+
* @description Reads a deployment's env file for an environment, with its OCI overlay applied
|
|
537
|
+
* when one exists and the caller is resolving for a container runtime.
|
|
538
|
+
*
|
|
539
|
+
* Single source of the overlay precedence: {@link loadConf} materializes the working tree from
|
|
540
|
+
* it and {@link UnderpostApp} projects the cluster Secret from it, so the file a container reads
|
|
541
|
+
* and the Secret injected into it always carry the same values. Outside a container the base
|
|
542
|
+
* file is returned untouched, which is what keeps a developer host on its local endpoints.
|
|
543
|
+
* @param {string} deployId - Deployment id.
|
|
544
|
+
* @param {string} [env='production'] - Environment selector.
|
|
545
|
+
* @param {string} [subConf=''] - Sub-configuration name.
|
|
546
|
+
* @param {object} [options] - Resolution options.
|
|
547
|
+
* @param {boolean} [options.oci] - Force the overlay on or off; defaults to container detection.
|
|
548
|
+
* @returns {{source: string, overlay: string|null, content: string, values: Object<string,string>}}
|
|
549
|
+
* The base file used, the overlay applied (or `null`), and the resulting contents and values.
|
|
550
|
+
* @memberof ServerConfBuilder
|
|
551
|
+
*/
|
|
552
|
+
const deployEnvContentFactory = (deployId, env = 'production', subConf = '', options = {}) => {
|
|
553
|
+
const source = deployEnvFilePath(deployId, env, subConf);
|
|
554
|
+
const baseContent = fs.readFileSync(source, 'utf8');
|
|
555
|
+
const overlayPath = deployOciEnvFilePath(deployId, env);
|
|
556
|
+
const applyOverlay = (options.oci ?? isOciRuntime()) && fs.existsSync(overlayPath);
|
|
557
|
+
if (!applyOverlay) return { source, overlay: null, content: baseContent, values: dotenv.parse(baseContent) };
|
|
558
|
+
const content = ociEnvContentFactory(baseContent, fs.readFileSync(overlayPath, 'utf8'));
|
|
559
|
+
return { source, overlay: overlayPath, content, values: dotenv.parse(content) };
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* @method cleanDeployEnvFiles
|
|
564
|
+
* @description Removes the working-tree env files {@link loadConf} materializes.
|
|
565
|
+
* Single source of the file list, shared with the app domain's `clean` action.
|
|
566
|
+
* @returns {Array<string>} The paths that were present and removed.
|
|
567
|
+
* @memberof ServerConfBuilder
|
|
568
|
+
*/
|
|
569
|
+
const cleanDeployEnvFiles = () => {
|
|
570
|
+
const targets = ['./.env', './.env.production', './.env.development', './.env.test'];
|
|
571
|
+
const present = targets.filter((target) => fs.existsSync(target));
|
|
572
|
+
for (const target of present) fs.removeSync(target);
|
|
573
|
+
return present;
|
|
574
|
+
};
|
|
575
|
+
|
|
415
576
|
const loadConf = (deployId = DEFAULT_DEPLOY_ID, subConf) => {
|
|
416
577
|
if (deployId === 'current') {
|
|
417
578
|
console.log(process.env.DEPLOY_ID);
|
|
418
579
|
return;
|
|
419
580
|
}
|
|
420
581
|
if (deployId === 'clean') {
|
|
421
|
-
|
|
422
|
-
fs.removeSync(`${path}/.env`);
|
|
423
|
-
fs.removeSync(`${path}/.env.production`);
|
|
424
|
-
fs.removeSync(`${path}/.env.development`);
|
|
425
|
-
fs.removeSync(`${path}/.env.test`);
|
|
582
|
+
cleanDeployEnvFiles();
|
|
426
583
|
return;
|
|
427
584
|
}
|
|
428
585
|
const folder = getConfFolder(deployId);
|
|
@@ -432,34 +589,58 @@ const loadConf = (deployId = DEFAULT_DEPLOY_ID, subConf) => {
|
|
|
432
589
|
if (subConf) process.env.DEPLOY_SUB_CONF = subConf;
|
|
433
590
|
|
|
434
591
|
for (const typeConf of Object.keys(Config.default)) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
592
|
+
const confPath = `${folder}/conf.${typeConf}.json`;
|
|
593
|
+
let srcConf;
|
|
594
|
+
if (fs.existsSync(confPath)) srcConf = fs.readFileSync(confPath, 'utf8');
|
|
595
|
+
else {
|
|
596
|
+
Config.default[typeConf] = newInstance(DefaultConf[typeConf]);
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
// Same precedence as getConfFilePath, so loadConf and every later read agree on which file
|
|
600
|
+
// is in effect: a sub-conf named here was named by the caller, so it is not gated on the
|
|
601
|
+
// ambient environment.
|
|
602
|
+
if (typeConf === 'server' && subConf) {
|
|
603
|
+
const devConfPath = getConfFilePath(deployId, typeConf, subConf);
|
|
438
604
|
if (fs.existsSync(devConfPath)) srcConf = fs.readFileSync(devConfPath, 'utf8');
|
|
439
605
|
}
|
|
440
606
|
let parsed = JSON.parse(srcConf);
|
|
441
607
|
if (typeConf === 'server') parsed = loadReplicas(deployId, parsed);
|
|
442
608
|
Config.default[typeConf] = parsed;
|
|
443
609
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
610
|
+
// Each materialized copy carries its own OCI overlay, so a container that later switches
|
|
611
|
+
// NODE_ENV still reads container values rather than the host endpoints of the base file.
|
|
612
|
+
const available = deployEnvNamesFactory(deployId);
|
|
613
|
+
for (const name of available)
|
|
614
|
+
fs.writeFileSync(`./.env.${name}`, deployEnvContentFactory(deployId, name).content, 'utf8');
|
|
615
|
+
|
|
616
|
+
const envName = process.env.NODE_ENV || 'development';
|
|
617
|
+
// A tree with no environment of its own is a workload: its configuration is injected, and
|
|
618
|
+
// materializing is neither possible nor wanted. A tree that carries environments but not this
|
|
619
|
+
// one is a mistake worth naming — falling through would run the deployment against whichever
|
|
620
|
+
// environment happened to be loaded.
|
|
621
|
+
if (available.length > 0) {
|
|
622
|
+
if (!available.includes(envName))
|
|
623
|
+
throw new Error(
|
|
624
|
+
configRejectionFactory({
|
|
625
|
+
domain: 'app',
|
|
626
|
+
deployId,
|
|
627
|
+
env: envName,
|
|
628
|
+
key: 'NODE_ENV',
|
|
629
|
+
reason: `no environment source; ${getConfFolder(deployId)} carries ${available.join(', ')}`,
|
|
630
|
+
}),
|
|
631
|
+
);
|
|
632
|
+
const { content, values } = deployEnvContentFactory(deployId, envName, subConf);
|
|
633
|
+
fs.writeFileSync(`./.env`, content, 'utf8');
|
|
634
|
+
process.env = { ...process.env, ...values, NODE_ENV: envName };
|
|
635
|
+
} else process.env.NODE_ENV = envName;
|
|
459
636
|
const originPackageJson = JSON.parse(fs.readFileSync(`./package.json`, 'utf8'));
|
|
460
637
|
const packageJson = JSON.parse(fs.readFileSync(`${folder}/package.json`, 'utf8'));
|
|
461
638
|
originPackageJson.scripts.start = packageJson.scripts.start;
|
|
462
639
|
packageJson.scripts = originPackageJson.scripts;
|
|
640
|
+
// `bin` is engine identity, not deploy identity: it is what `npm link` publishes as the
|
|
641
|
+
// global `underpost` command. Deploy manifests do not carry one, so without this the
|
|
642
|
+
// checkout would stop being linkable the moment its environment was loaded.
|
|
643
|
+
if (originPackageJson.bin) packageJson.bin = originPackageJson.bin;
|
|
463
644
|
fs.writeFileSync(`./package.json`, JSON.stringify(packageJson, null, 4), 'utf8');
|
|
464
645
|
return { folder, deployId };
|
|
465
646
|
};
|
|
@@ -830,247 +1011,6 @@ const cloneSrcComponents = async ({ toOptions, fromOptions }) => {
|
|
|
830
1011
|
}
|
|
831
1012
|
};
|
|
832
1013
|
|
|
833
|
-
/**
|
|
834
|
-
* @method buildProxyRouter
|
|
835
|
-
* @description Builds the proxy router.
|
|
836
|
-
* @memberof ServerConfBuilder
|
|
837
|
-
*/
|
|
838
|
-
const buildProxyRouter = () => {
|
|
839
|
-
const confServer = newInstance(Config.default.server);
|
|
840
|
-
let currentPort = parseInt(process.env.PORT) + 1;
|
|
841
|
-
const proxyRouter = {};
|
|
842
|
-
for (const host of Object.keys(confServer)) {
|
|
843
|
-
for (const path of Object.keys(confServer[host])) {
|
|
844
|
-
if (confServer[host][path].singleReplica) continue;
|
|
845
|
-
|
|
846
|
-
if (isDevProxyContext()) confServer[host][path].proxy = [isTlsDevProxy() ? 443 : 80];
|
|
847
|
-
|
|
848
|
-
confServer[host][path].port = newInstance(currentPort);
|
|
849
|
-
for (const port of confServer[host][path].proxy) {
|
|
850
|
-
if (!(port in proxyRouter)) proxyRouter[port] = {};
|
|
851
|
-
proxyRouter[port][`${host}${path}`] = {
|
|
852
|
-
// target: `http://${host}:${confServer[host][path].port}${path}`,
|
|
853
|
-
target: `http://localhost:${confServer[host][path].port}`,
|
|
854
|
-
// target: `http://127.0.0.1:${confServer[host][path].port}`,
|
|
855
|
-
proxy: confServer[host][path].proxy,
|
|
856
|
-
redirect: confServer[host][path].redirect,
|
|
857
|
-
host,
|
|
858
|
-
path,
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
currentPort++;
|
|
862
|
-
if (confServer[host][path].peer) {
|
|
863
|
-
const peerPath = path === '/' ? `/peer` : `${path}/peer`;
|
|
864
|
-
confServer[host][peerPath] = newInstance(confServer[host][path]);
|
|
865
|
-
confServer[host][peerPath].port = newInstance(currentPort);
|
|
866
|
-
for (const port of confServer[host][path].proxy) {
|
|
867
|
-
if (!(port in proxyRouter)) proxyRouter[port] = {};
|
|
868
|
-
proxyRouter[port][`${host}${peerPath}`] = {
|
|
869
|
-
// target: `http://${host}:${confServer[host][peerPath].port}${peerPath}`,
|
|
870
|
-
target: `http://localhost:${confServer[host][peerPath].port}`,
|
|
871
|
-
// target: `http://127.0.0.1:${confServer[host][peerPath].port}`,
|
|
872
|
-
proxy: confServer[host][peerPath].proxy,
|
|
873
|
-
host,
|
|
874
|
-
path: peerPath,
|
|
875
|
-
};
|
|
876
|
-
}
|
|
877
|
-
currentPort++;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
return proxyRouter;
|
|
883
|
-
};
|
|
884
|
-
|
|
885
|
-
/**
|
|
886
|
-
* @method pathPortAssignmentFactory
|
|
887
|
-
* @description Creates the path port assignment.
|
|
888
|
-
* @param {string} deployId - The deploy ID.
|
|
889
|
-
* @param {object} router - The router.
|
|
890
|
-
* @param {object} confServer - The server configuration.
|
|
891
|
-
* @memberof ServerConfBuilder
|
|
892
|
-
*/
|
|
893
|
-
const pathPortAssignmentFactory = async (deployId, router, confServer) => {
|
|
894
|
-
const pathPortAssignmentData = {};
|
|
895
|
-
for (const host of Object.keys(confServer)) {
|
|
896
|
-
const pathPortAssignment = [];
|
|
897
|
-
for (const path of Object.keys(confServer[host])) {
|
|
898
|
-
const { peer } = confServer[host][path];
|
|
899
|
-
if (!router[`${host}${path === '/' ? '' : path}`]) continue;
|
|
900
|
-
const port = parseInt(router[`${host}${path === '/' ? '' : path}`].split(':')[2]);
|
|
901
|
-
// logger.info('', { host, port, path });
|
|
902
|
-
pathPortAssignment.push({
|
|
903
|
-
port,
|
|
904
|
-
path,
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
if (peer) {
|
|
908
|
-
// logger.info('', { host, port: port + 1, path: '/peer' });
|
|
909
|
-
pathPortAssignment.push({
|
|
910
|
-
port: port + 1,
|
|
911
|
-
path: `${path === '/' ? '' : path}/peer`,
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
pathPortAssignmentData[host] = pathPortAssignment;
|
|
916
|
-
}
|
|
917
|
-
if (fs.existsSync(`./engine-private/replica`)) {
|
|
918
|
-
const singleReplicas = await fs.readdir(`./engine-private/replica`);
|
|
919
|
-
for (let replica of singleReplicas) {
|
|
920
|
-
if (replica.startsWith(deployId)) {
|
|
921
|
-
const replicaServerConf = loadConfServerJson(`./engine-private/replica/${replica}/conf.server.json`);
|
|
922
|
-
for (const host of Object.keys(replicaServerConf)) {
|
|
923
|
-
const pathPortAssignment = [];
|
|
924
|
-
for (const path of Object.keys(replicaServerConf[host])) {
|
|
925
|
-
const { peer } = replicaServerConf[host][path];
|
|
926
|
-
if (!router[`${host}${path === '/' ? '' : path}`]) continue;
|
|
927
|
-
const port = parseInt(router[`${host}${path === '/' ? '' : path}`].split(':')[2]);
|
|
928
|
-
// logger.info('', { host, port, path });
|
|
929
|
-
pathPortAssignment.push({
|
|
930
|
-
port,
|
|
931
|
-
path,
|
|
932
|
-
});
|
|
933
|
-
|
|
934
|
-
if (peer) {
|
|
935
|
-
// logger.info('', { host, port: port + 1, path: '/peer' });
|
|
936
|
-
pathPortAssignment.push({
|
|
937
|
-
port: port + 1,
|
|
938
|
-
path: `${path === '/' ? '' : path}/peer`,
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
pathPortAssignmentData[host] = pathPortAssignmentData[host].concat(pathPortAssignment);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
return pathPortAssignmentData;
|
|
948
|
-
};
|
|
949
|
-
|
|
950
|
-
/**
|
|
951
|
-
* @method deployRangePortFactory
|
|
952
|
-
* @description Creates the deploy range port factory.
|
|
953
|
-
* @param {object} router - The router.
|
|
954
|
-
* @returns {object} - The deploy range port factory.
|
|
955
|
-
* @memberof ServerConfBuilder
|
|
956
|
-
*/
|
|
957
|
-
const deployRangePortFactory = (router) => {
|
|
958
|
-
const ports = Object.values(router).map((p) => parseInt(p.split(':')[2]));
|
|
959
|
-
const fromPort = Math.min(...ports);
|
|
960
|
-
const toPort = Math.max(...ports);
|
|
961
|
-
return { ports, fromPort, toPort };
|
|
962
|
-
};
|
|
963
|
-
|
|
964
|
-
/**
|
|
965
|
-
* @method buildKindPorts
|
|
966
|
-
* @description Builds the kind ports.
|
|
967
|
-
* @param {number} from - The from port.
|
|
968
|
-
* @param {number} to - The to port.
|
|
969
|
-
* @returns {string} - The kind ports.
|
|
970
|
-
* @memberof ServerConfBuilder
|
|
971
|
-
*/
|
|
972
|
-
const buildKindPorts = (from, to) =>
|
|
973
|
-
range(parseInt(from), parseInt(to))
|
|
974
|
-
.map(
|
|
975
|
-
(port) => ` - name: 'tcp-${port}'
|
|
976
|
-
protocol: TCP
|
|
977
|
-
port: ${port}
|
|
978
|
-
targetPort: ${port}
|
|
979
|
-
- name: 'udp-${port}'
|
|
980
|
-
protocol: UDP
|
|
981
|
-
port: ${port}
|
|
982
|
-
targetPort: ${port}
|
|
983
|
-
`,
|
|
984
|
-
)
|
|
985
|
-
.join('\n');
|
|
986
|
-
|
|
987
|
-
/**
|
|
988
|
-
* @method buildPortProxyRouter
|
|
989
|
-
* @description Builds the port proxy router.
|
|
990
|
-
* @param {object} options - The options.
|
|
991
|
-
* @param {number} [options.port=4000] - The port.
|
|
992
|
-
* @param {object} options.proxyRouter - The proxy router.
|
|
993
|
-
* @param {object} [options.hosts] - The hosts.
|
|
994
|
-
* @param {boolean} [options.orderByPathLength=false] - Whether to order by path length.
|
|
995
|
-
* @param {boolean} [options.devProxyContext=false] - Whether to use dev proxy context.
|
|
996
|
-
* @returns {object} - The port proxy router.
|
|
997
|
-
* @memberof ServerConfBuilder
|
|
998
|
-
*/
|
|
999
|
-
const buildPortProxyRouter = (
|
|
1000
|
-
options = { port: 4000, proxyRouter, hosts, orderByPathLength: false, devProxyContext: false },
|
|
1001
|
-
) => {
|
|
1002
|
-
let { port, proxyRouter, hosts, orderByPathLength } = options;
|
|
1003
|
-
hosts = hosts || proxyRouter[port] || {};
|
|
1004
|
-
|
|
1005
|
-
const router = {};
|
|
1006
|
-
// build router
|
|
1007
|
-
Object.keys(hosts).map((hostKey) => {
|
|
1008
|
-
let { host, path, target, proxy, peer } = hosts[hostKey];
|
|
1009
|
-
|
|
1010
|
-
if (!proxy.includes(port)) {
|
|
1011
|
-
logger.warn('Proxy port not set on conf', { port, host, path, proxy, target });
|
|
1012
|
-
if (process.env.NODE_ENV === 'production') {
|
|
1013
|
-
logger.warn('Omitting host', { host, path, target });
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
// ${process.env.NODE_ENV === 'development' && !isDevProxyContext() ? `:${port}` : ''}
|
|
1018
|
-
const absoluteHost = [80, 443].includes(port)
|
|
1019
|
-
? `${host}${path === '/' ? '' : path}`
|
|
1020
|
-
: `${host}:${port}${path === '/' ? '' : path}`;
|
|
1021
|
-
|
|
1022
|
-
if (absoluteHost in router)
|
|
1023
|
-
logger.warn('Overwrite: Absolute host already exists on router', { absoluteHost, target });
|
|
1024
|
-
|
|
1025
|
-
if (options.devProxyContext === true) {
|
|
1026
|
-
const appDevPort = parseInt(target.split(':')[2]) - process.env.DEV_PROXY_PORT_OFFSET;
|
|
1027
|
-
router[absoluteHost] = `http://localhost:${appDevPort}`;
|
|
1028
|
-
} else router[absoluteHost] = target;
|
|
1029
|
-
}); // order router
|
|
1030
|
-
|
|
1031
|
-
if (Object.keys(router).length === 0) return router;
|
|
1032
|
-
|
|
1033
|
-
const devApiConfPath = `./engine-private/conf/${process.argv[3]}/conf.server.dev.${process.argv[4]}-dev-api.json`;
|
|
1034
|
-
if (options.devProxyContext === true && process.env.NODE_ENV === 'development' && fs.existsSync(devApiConfPath)) {
|
|
1035
|
-
const confDevApiServer = JSON.parse(fs.readFileSync(devApiConfPath, 'utf8'));
|
|
1036
|
-
let devApiHosts = [];
|
|
1037
|
-
let origins = [];
|
|
1038
|
-
for (const _host of Object.keys(confDevApiServer))
|
|
1039
|
-
for (const _path of Object.keys(confDevApiServer[_host])) {
|
|
1040
|
-
if (confDevApiServer[_host][_path].origins && confDevApiServer[_host][_path].origins.length) {
|
|
1041
|
-
origins.push(...confDevApiServer[_host][_path].origins);
|
|
1042
|
-
if (_path !== 'peer' && devApiHosts.length === 0)
|
|
1043
|
-
devApiHosts.push(
|
|
1044
|
-
`${_host}${[80, 443].includes(port) && isDevProxyContext() ? '' : `:${port}`}${_path == '/' ? '' : _path}`,
|
|
1045
|
-
);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
origins = Array.from(new Set(origins));
|
|
1049
|
-
console.log({
|
|
1050
|
-
origins,
|
|
1051
|
-
devApiHosts,
|
|
1052
|
-
});
|
|
1053
|
-
for (const devApiHost of devApiHosts) {
|
|
1054
|
-
if (devApiHost in router) {
|
|
1055
|
-
const target = router[devApiHost];
|
|
1056
|
-
delete router[devApiHost];
|
|
1057
|
-
router[`${devApiHost}/${process.env.BASE_API}`] = target;
|
|
1058
|
-
router[`${devApiHost}/socket.io`] = target;
|
|
1059
|
-
for (const origin of origins) router[devApiHost] = origin;
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
if (orderByPathLength === true) {
|
|
1065
|
-
const reOrderRouter = {};
|
|
1066
|
-
for (const absoluteHostKey of orderArrayFromAttrInt(Object.keys(router), 'length'))
|
|
1067
|
-
reOrderRouter[absoluteHostKey] = router[absoluteHostKey];
|
|
1068
|
-
return reOrderRouter;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
return router;
|
|
1072
|
-
};
|
|
1073
|
-
|
|
1074
1014
|
/**
|
|
1075
1015
|
* @method buildReplicaId
|
|
1076
1016
|
* @description Builds the replica ID.
|
|
@@ -1098,17 +1038,7 @@ const getDataDeploy = async (
|
|
|
1098
1038
|
disableSyncEnvPort: false,
|
|
1099
1039
|
},
|
|
1100
1040
|
) => {
|
|
1101
|
-
let dataDeploy =
|
|
1102
|
-
.readFileSync(`./engine-private/deploy/dd.router`, 'utf8')
|
|
1103
|
-
.split(',')
|
|
1104
|
-
.map((deployId) => deployId.trim())
|
|
1105
|
-
.filter((deployId) => deployId);
|
|
1106
|
-
|
|
1107
|
-
dataDeploy = dataDeploy.map((deployId) => {
|
|
1108
|
-
return {
|
|
1109
|
-
deployId,
|
|
1110
|
-
};
|
|
1111
|
-
});
|
|
1041
|
+
let dataDeploy = readDeployRoutes().map((deployId) => ({ deployId }));
|
|
1112
1042
|
|
|
1113
1043
|
if (options && options.buildSingleReplica && fs.existsSync(`./engine-private/replica`))
|
|
1114
1044
|
fs.removeSync(`./engine-private/replica`);
|
|
@@ -1125,7 +1055,7 @@ const getDataDeploy = async (
|
|
|
1125
1055
|
for (const path of Object.keys(serverConf[host])) {
|
|
1126
1056
|
if (!isReplicaDeploy && serverConf[host][path].replicas && serverConf[host][path].singleReplica) {
|
|
1127
1057
|
if (options && options.buildSingleReplica)
|
|
1128
|
-
await Underpost.
|
|
1058
|
+
await Underpost.client.callback(deployObj.deployId, '', host, path, {
|
|
1129
1059
|
singleReplica: true,
|
|
1130
1060
|
});
|
|
1131
1061
|
replicaDataDeploy = replicaDataDeploy.concat(
|
|
@@ -1143,7 +1073,7 @@ const getDataDeploy = async (
|
|
|
1143
1073
|
}
|
|
1144
1074
|
|
|
1145
1075
|
if (!options.disableSyncEnvPort && options.buildSingleReplica)
|
|
1146
|
-
await Underpost.
|
|
1076
|
+
await Underpost.client.callback(undefined, '', '', '', { syncEnvPort: true });
|
|
1147
1077
|
|
|
1148
1078
|
logger.info('Deployments configured', buildDataDeploy);
|
|
1149
1079
|
|
|
@@ -1168,7 +1098,7 @@ const validateTemplatePath = (absolutePath = '') => {
|
|
|
1168
1098
|
const clients = DefaultConf.client.default.services;
|
|
1169
1099
|
|
|
1170
1100
|
if (
|
|
1171
|
-
absolutePath.match('src/api') &&
|
|
1101
|
+
absolutePath.match('src/api/') &&
|
|
1172
1102
|
!absolutePath.match('src/api/types.js') &&
|
|
1173
1103
|
!confServer.apis.find((p) => absolutePath.match(`src/api/${p}/`))
|
|
1174
1104
|
) {
|
|
@@ -1243,11 +1173,11 @@ const validateTemplatePath = (absolutePath = '') => {
|
|
|
1243
1173
|
* @memberof ServerConfBuilder
|
|
1244
1174
|
*/
|
|
1245
1175
|
const awaitDeployMonitor = async (isFinal = false, deltaMs = 1000, callback = false) => {
|
|
1246
|
-
if (!callback)
|
|
1176
|
+
if (!callback) latchAwaitDeploy();
|
|
1247
1177
|
if (isFinal) logger.info('Final deployment running (no replica)');
|
|
1248
1178
|
await timer(deltaMs);
|
|
1249
|
-
if (
|
|
1250
|
-
if (
|
|
1179
|
+
if (getRuntimeStatus() === RUNTIME_STATUS.ERROR) return false;
|
|
1180
|
+
if (isAwaitingDeploy()) return await awaitDeployMonitor(false, deltaMs, true);
|
|
1251
1181
|
return true;
|
|
1252
1182
|
};
|
|
1253
1183
|
|
|
@@ -1564,35 +1494,49 @@ const getInstanceContext = async (options = { deployId, singleReplica, replicas,
|
|
|
1564
1494
|
* @param {string} options.host - The host.
|
|
1565
1495
|
* @param {string} options.path - The path.
|
|
1566
1496
|
* @param {string} options.origin - The origin.
|
|
1567
|
-
* @returns {
|
|
1497
|
+
* @returns {Promise<string|undefined>} The derived sub-conf id (`<subConf>-dev-api`), or
|
|
1498
|
+
* undefined when no client origin was given and nothing was derived.
|
|
1568
1499
|
* @memberof ServerConfBuilder
|
|
1569
1500
|
*/
|
|
1570
1501
|
const buildApiConf = async (options = { deployId: '', subConf: '', host: '', path: '', origin: '' }) => {
|
|
1571
1502
|
let { deployId, subConf, host, path, origin } = options;
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
if (process.argv[4]) host = process.argv[4].trim();
|
|
1575
|
-
if (process.argv[5]) path = process.argv[5].trim();
|
|
1503
|
+
// The client origin is what selects this mode, so it is resolved before
|
|
1504
|
+
// anything else: `src/api.js` also runs without any of these positionals.
|
|
1576
1505
|
if (process.argv[6])
|
|
1577
1506
|
origin = `${process.env.NODE_ENV === 'production' ? 'https' : 'http'}://${process.argv[6].trim()}`;
|
|
1578
|
-
|
|
1579
|
-
if (!
|
|
1507
|
+
if (!origin) return undefined;
|
|
1508
|
+
if (!deployId) deployId = process.argv[2]?.trim();
|
|
1509
|
+
if (!subConf) subConf = process.argv[3]?.trim();
|
|
1510
|
+
if (process.argv[4]) host = process.argv[4].trim();
|
|
1511
|
+
if (process.argv[5]) path = process.argv[5].trim();
|
|
1512
|
+
if (!deployId || !subConf || !host || !path) {
|
|
1513
|
+
logger.warn('Skipping api conf; a client origin needs deploy-id, sub-conf, host and path', {
|
|
1514
|
+
deployId,
|
|
1515
|
+
subConf,
|
|
1516
|
+
host,
|
|
1517
|
+
path,
|
|
1518
|
+
origin,
|
|
1519
|
+
});
|
|
1520
|
+
return undefined;
|
|
1521
|
+
}
|
|
1580
1522
|
const confServer = JSON.parse(
|
|
1581
1523
|
fs.readFileSync(`./engine-private/conf/${deployId}/conf.server.dev.${subConf}.json`, 'utf8'),
|
|
1582
1524
|
);
|
|
1583
1525
|
const envObj = dotenv.parse(
|
|
1584
1526
|
fs.readFileSync(`./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}`, 'utf8'),
|
|
1585
1527
|
);
|
|
1586
|
-
if (host
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1528
|
+
if (!confServer[host]?.[path])
|
|
1529
|
+
throw new Error(`No ${host}${path} instance in conf.server.dev.${subConf}.json for ${deployId}`);
|
|
1530
|
+
confServer[host][path].origins = [origin];
|
|
1531
|
+
logger.info('Build api conf', { host, path, origin });
|
|
1532
|
+
const devApiSubConf = `${subConf}-dev-api`;
|
|
1533
|
+
writeEnv(`./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}.${devApiSubConf}`, envObj);
|
|
1591
1534
|
fs.writeFileSync(
|
|
1592
|
-
`./engine-private/conf/${deployId}/conf.server.dev.${
|
|
1535
|
+
`./engine-private/conf/${deployId}/conf.server.dev.${devApiSubConf}.json`,
|
|
1593
1536
|
JSON.stringify(confServer, null, 4),
|
|
1594
1537
|
'utf8',
|
|
1595
1538
|
);
|
|
1539
|
+
return devApiSubConf;
|
|
1596
1540
|
};
|
|
1597
1541
|
|
|
1598
1542
|
/**
|
|
@@ -1616,12 +1560,19 @@ const buildClientStaticConf = async (
|
|
|
1616
1560
|
if (!subConf) subConf = process.argv[3].trim();
|
|
1617
1561
|
if (!host) host = process.argv[4].trim();
|
|
1618
1562
|
if (!path) path = process.argv[5].trim();
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
const
|
|
1623
|
-
|
|
1624
|
-
)
|
|
1563
|
+
// The API server derives these two files, and it derives them only when it is
|
|
1564
|
+
// given the client origin. Their absence means that step has not run, which is
|
|
1565
|
+
// a setup order to state rather than an ENOENT to decode.
|
|
1566
|
+
const confPath = `./engine-private/conf/${deployId}/conf.server.dev.${subConf}-dev-api.json`;
|
|
1567
|
+
const envPath = `./engine-private/conf/${deployId}/.env.${process.env.NODE_ENV}.${subConf}-dev-api`;
|
|
1568
|
+
for (const source of [confPath, envPath])
|
|
1569
|
+
if (!fs.existsSync(source))
|
|
1570
|
+
throw new Error(
|
|
1571
|
+
`Missing ${source}. Start the API server first, with the client origin as its last argument: ` +
|
|
1572
|
+
`npm run dev:api ${deployId} ${subConf} ${host} ${path} <client-host:port>`,
|
|
1573
|
+
);
|
|
1574
|
+
const confServer = JSON.parse(fs.readFileSync(confPath, 'utf8'));
|
|
1575
|
+
const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
|
|
1625
1576
|
envObj.PORT = parseInt(envObj.PORT);
|
|
1626
1577
|
const apiBaseHost = devProxy
|
|
1627
1578
|
? devProxyHostFactory({ host, tls: isTlsDevProxy() })
|
|
@@ -1640,16 +1591,6 @@ const buildClientStaticConf = async (
|
|
|
1640
1591
|
);
|
|
1641
1592
|
};
|
|
1642
1593
|
|
|
1643
|
-
/**
|
|
1644
|
-
* @method isDeployRunnerContext
|
|
1645
|
-
* @description Checks if the deploy runner context is valid.
|
|
1646
|
-
* @param {string} path - The path.
|
|
1647
|
-
* @param {object} options - The options.
|
|
1648
|
-
* @returns {boolean} - The deploy runner context.
|
|
1649
|
-
* @memberof ServerConfBuilder
|
|
1650
|
-
*/
|
|
1651
|
-
const isDeployRunnerContext = (path, options) => !options.build && path && path !== 'template-deploy';
|
|
1652
|
-
|
|
1653
1594
|
/**
|
|
1654
1595
|
* @method isDevProxyContext
|
|
1655
1596
|
* @description Checks if the dev proxy context is valid.
|
|
@@ -1860,7 +1801,7 @@ const dispatchBuildInstanceEnv = ({
|
|
|
1860
1801
|
const loadProjectInstanceEnvBuilder = async (deployId) => {
|
|
1861
1802
|
const match = /^dd-([a-z0-9][a-z0-9-]*)$/.exec(`${deployId || ''}`);
|
|
1862
1803
|
if (!match) return null;
|
|
1863
|
-
const moduleUrl = new URL(
|
|
1804
|
+
const moduleUrl = new URL(`../../projects/${match[1]}/instance-data.js`, import.meta.url);
|
|
1864
1805
|
if (!fs.existsSync(moduleUrl)) return null;
|
|
1865
1806
|
const projectModule = await import(moduleUrl.href);
|
|
1866
1807
|
if (projectModule.buildInstanceEnv === undefined) return null;
|
|
@@ -2382,6 +2323,56 @@ const curlStatusChainFactory = (raw = '') => {
|
|
|
2382
2323
|
return chain;
|
|
2383
2324
|
};
|
|
2384
2325
|
|
|
2326
|
+
/**
|
|
2327
|
+
* @method publicIngressProbeFactory
|
|
2328
|
+
* @description Probes one public URL the way the traffic report does.
|
|
2329
|
+
*
|
|
2330
|
+
* `-L` follows the real redirect chain, `-v` supplies one response line per hop,
|
|
2331
|
+
* `-i` keeps full response headers available, and `-s` removes only the progress
|
|
2332
|
+
* meter. The body is discarded so a report stays bounded even when a host
|
|
2333
|
+
* returns a large application page. One implementation, because the traffic
|
|
2334
|
+
* table and the ingress event have to agree on what "reachable" means.
|
|
2335
|
+
* @param {string} url - Absolute URL to probe.
|
|
2336
|
+
* @returns {Array<string>} Ordered response codes; `['000']` when nothing answered.
|
|
2337
|
+
* @memberof ServerConfBuilder
|
|
2338
|
+
*/
|
|
2339
|
+
const publicIngressProbeFactory = (url) => {
|
|
2340
|
+
if (!url) return ['000'];
|
|
2341
|
+
const argument = shellArgumentFactory(url);
|
|
2342
|
+
return curlStatusChainFactory(
|
|
2343
|
+
shellExec(
|
|
2344
|
+
`curl -L -v -i -s --connect-timeout 3 --max-time 12 --max-redirs 10 ` +
|
|
2345
|
+
`-o /dev/null -w '\nUNDERPOST_CURL_FINAL=%{http_code}\n' ${argument} 2>&1 || true`,
|
|
2346
|
+
{ stdout: true, silent: true, silentOnError: true, disableLog: true },
|
|
2347
|
+
),
|
|
2348
|
+
);
|
|
2349
|
+
};
|
|
2350
|
+
|
|
2351
|
+
/**
|
|
2352
|
+
* @method publicIngressUrlsFactory
|
|
2353
|
+
* @description Every public URL a deploy list publishes, from the same conf the
|
|
2354
|
+
* traffic report reads.
|
|
2355
|
+
* @param {string[]} [deployList] - Deploy ids.
|
|
2356
|
+
* @param {string} [env='production'] - Environment whose routes are published.
|
|
2357
|
+
* @returns {Array<{host: string, path: string, url: string}>} Distinct routable URLs.
|
|
2358
|
+
* @memberof ServerConfBuilder
|
|
2359
|
+
*/
|
|
2360
|
+
const publicIngressUrlsFactory = (deployList = [], env = 'production') => {
|
|
2361
|
+
const urls = new Map();
|
|
2362
|
+
for (const deployId of deployList)
|
|
2363
|
+
for (const entry of deployTrafficEntriesFactory({ deployId, env }))
|
|
2364
|
+
for (const routePath of `${entry.path || '/'}`.split(' ').filter(Boolean)) {
|
|
2365
|
+
const path = routePath.startsWith('/') ? routePath : `/${routePath}`;
|
|
2366
|
+
try {
|
|
2367
|
+
const url = new URL(path, `https://${entry.host}`).href;
|
|
2368
|
+
if (!urls.has(url)) urls.set(url, { host: entry.host, path, url });
|
|
2369
|
+
} catch {
|
|
2370
|
+
/* a host the conf cannot turn into a URL is not routable */
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
return [...urls.values()];
|
|
2374
|
+
};
|
|
2375
|
+
|
|
2385
2376
|
/**
|
|
2386
2377
|
* @method trafficTableRowsFactory
|
|
2387
2378
|
* @description Resolves the live colour of each routable deployment, optionally
|
|
@@ -2464,6 +2455,25 @@ const hostRenderInstancesFactory = ({ declared = [], preserved = [], isDeployed
|
|
|
2464
2455
|
const isTrafficServingFactory = ({ liveTraffic = '', hasReadyEndpoints = () => false }) =>
|
|
2465
2456
|
!!liveTraffic && hasReadyEndpoints(liveTraffic);
|
|
2466
2457
|
|
|
2458
|
+
/**
|
|
2459
|
+
* @method redeployPlanFactory
|
|
2460
|
+
* @description The colour a redeploy targets, and how that colour is brought up.
|
|
2461
|
+
*
|
|
2462
|
+
* The inactive colour has no Deployment for most of a blue/green cycle: `run sync`
|
|
2463
|
+
* deletes it before publishing the maintenance route, and a promote leaves only
|
|
2464
|
+
* the colour it routed to. So a `rollout restart` of the target is a NotFound
|
|
2465
|
+
* error on exactly the state blue/green is meant to be in, and its absence has to
|
|
2466
|
+
* select building that colour instead of failing the deploy.
|
|
2467
|
+
* @param {string} [liveTraffic] - Colour currently routed, or empty when none is.
|
|
2468
|
+
* @param {Function} hasDeployment - `(colour) => boolean`.
|
|
2469
|
+
* @returns {{targetTraffic: string, create: boolean}} Colour to deploy, and whether it must be created rather than restarted.
|
|
2470
|
+
* @memberof ServerConfBuilder
|
|
2471
|
+
*/
|
|
2472
|
+
const redeployPlanFactory = ({ liveTraffic = '', hasDeployment = () => false }) => {
|
|
2473
|
+
const targetTraffic = nextTrafficFactory(liveTraffic);
|
|
2474
|
+
return { targetTraffic, create: !hasDeployment(targetTraffic) };
|
|
2475
|
+
};
|
|
2476
|
+
|
|
2467
2477
|
/**
|
|
2468
2478
|
* @method instanceTrafficPlanFactory
|
|
2469
2479
|
* @description Resolves, for each instance, the colour routed now and the colour
|
|
@@ -2931,39 +2941,14 @@ ${renderHosts}`,
|
|
|
2931
2941
|
return { renderHosts, changed: true };
|
|
2932
2942
|
};
|
|
2933
2943
|
|
|
2934
|
-
/**
|
|
2935
|
-
* Resolves the concrete deploy ids a build or conf-sync run should iterate over.
|
|
2936
|
-
*
|
|
2937
|
-
* The meta deploy id `dd` fans out to the comma separated ids declared in
|
|
2938
|
-
* `engine-private/deploy/dd.router`; when that file is absent (e.g. the private
|
|
2939
|
-
* repository is not checked out) it falls back to {@link ServerConfBuilder.DEFAULT_DEPLOY_ID}.
|
|
2940
|
-
* Any other value is parsed as a comma separated list.
|
|
2941
|
-
* Entries are trimmed and empties dropped.
|
|
2942
|
-
*
|
|
2943
|
-
* @method resolveDeployList
|
|
2944
|
-
* @param {string} deployId - A deploy id, a comma separated list, or the `dd` meta id.
|
|
2945
|
-
* @returns {string[]} Ordered list of concrete deploy ids.
|
|
2946
|
-
* @memberof ServerConfBuilder
|
|
2947
|
-
*/
|
|
2948
|
-
const resolveDeployList = (deployId) =>
|
|
2949
|
-
(deployId === 'dd'
|
|
2950
|
-
? fs.existsSync('./engine-private/deploy/dd.router')
|
|
2951
|
-
? fs.readFileSync('./engine-private/deploy/dd.router', 'utf8')
|
|
2952
|
-
: DEFAULT_DEPLOY_ID
|
|
2953
|
-
: deployId
|
|
2954
|
-
)
|
|
2955
|
-
.split(',')
|
|
2956
|
-
.map((id) => id.trim())
|
|
2957
|
-
.filter(Boolean);
|
|
2958
|
-
|
|
2959
2944
|
/**
|
|
2960
2945
|
* Syncs a single deploy id's private configuration into its dedicated
|
|
2961
2946
|
* `engine-<suffix>-private` repository and pushes the result.
|
|
2962
2947
|
*
|
|
2963
2948
|
* Idempotent and safe to rerun: the private repo is cloned when missing or reset to a clean
|
|
2964
|
-
* checkout when present, then the deploy id's `conf` folder
|
|
2965
|
-
* `
|
|
2966
|
-
* commit/push step is a no-op when nothing changed (`silentOnError`).
|
|
2949
|
+
* checkout when present, then the deploy id's `conf` folder (its `.env.<env>.oci` runtime
|
|
2950
|
+
* overlays included), matching `replica` entries, and any caller-supplied `extraPaths` payloads
|
|
2951
|
+
* are mirrored. The commit/push step is a no-op when nothing changed (`silentOnError`).
|
|
2967
2952
|
*
|
|
2968
2953
|
* @method syncPrivateConf
|
|
2969
2954
|
* @param {string} deployId - A concrete deploy id (e.g. `dd-cyberia`), not the `dd` meta id.
|
|
@@ -2973,10 +2958,10 @@ const resolveDeployList = (deployId) =>
|
|
|
2973
2958
|
* @memberof ServerConfBuilder
|
|
2974
2959
|
*/
|
|
2975
2960
|
const syncPrivateConf = (deployId, extraPaths = []) => {
|
|
2976
|
-
const
|
|
2977
|
-
const privateRepoName = `engine-${suffix}-private`;
|
|
2961
|
+
const privateRepoName = Underpost.repo.privateRepoFactory(deployId);
|
|
2978
2962
|
const privateGitUri = `${process.env.GITHUB_USERNAME}/${privateRepoName}`;
|
|
2979
2963
|
const privateRepoPath = `../${privateRepoName}`;
|
|
2964
|
+
const cronDeployId = cronDeployIdResolve();
|
|
2980
2965
|
|
|
2981
2966
|
if (!fs.existsSync(privateRepoPath)) {
|
|
2982
2967
|
shellExec(`cd .. && underpost clone ${privateGitUri}`, { silent: true });
|
|
@@ -2991,17 +2976,21 @@ const syncPrivateConf = (deployId, extraPaths = []) => {
|
|
|
2991
2976
|
fs.removeSync(confDest);
|
|
2992
2977
|
fs.mkdirSync(confDest, { recursive: true });
|
|
2993
2978
|
fs.copySync(`./engine-private/conf/${deployId}`, confDest);
|
|
2994
|
-
|
|
2979
|
+
// A checkout that declares no cron deploy has no `conf/<cron-id>` to mirror. Without this
|
|
2980
|
+
// the path resolves to `conf/null` and the whole sync fails over an absent optional.
|
|
2981
|
+
if (cronDeployId) fs.copySync(`./engine-private/conf/${cronDeployId}`, `${privateRepoPath}/conf/${cronDeployId}`);
|
|
2982
|
+
fs.copySync(`./engine-private/deploy`, `${privateRepoPath}/deploy`);
|
|
2995
2983
|
fs.removeSync(`${privateRepoPath}/replica`);
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
for (const entry of fs.readdirSync(
|
|
3000
|
-
if (entry.match(deployId)) fs.copySync(`${
|
|
2984
|
+
fs.removeSync(`${privateRepoPath}/logs`);
|
|
2985
|
+
const replicaSrcDir = `./engine-private/replica`;
|
|
2986
|
+
if (fs.existsSync(replicaSrcDir))
|
|
2987
|
+
for (const entry of fs.readdirSync(replicaSrcDir))
|
|
2988
|
+
if (entry.match(deployId)) fs.copySync(`${replicaSrcDir}/${entry}`, `${privateRepoPath}/replica/${entry}`);
|
|
2989
|
+
for (const extraPath of extraPaths) {
|
|
2990
|
+
fs.removeSync(`${privateRepoPath}/${extraPath}`);
|
|
2991
|
+
fs.copySync(`./engine-private/${extraPath}`, `${privateRepoPath}/${extraPath}`);
|
|
3001
2992
|
}
|
|
3002
2993
|
|
|
3003
|
-
for (const extraPath of extraPaths) fs.copySync(`./engine-private/${extraPath}`, `${privateRepoPath}/${extraPath}`);
|
|
3004
|
-
|
|
3005
2994
|
shellExec(
|
|
3006
2995
|
`cd ${privateRepoPath}` +
|
|
3007
2996
|
` && git add .` +
|
|
@@ -3031,31 +3020,111 @@ const syncDeployIdSources = (sourceMoves = []) => {
|
|
|
3031
3020
|
return true;
|
|
3032
3021
|
};
|
|
3033
3022
|
|
|
3023
|
+
const TEMPLATE_REPOSITORY_NAME = 'pwa-microservices-template';
|
|
3024
|
+
|
|
3025
|
+
/**
|
|
3026
|
+
* @method gitOriginRepositoryName
|
|
3027
|
+
* @description Repository name a checkout's `origin` points at, without the `.git`
|
|
3028
|
+
* suffix, or `null` when the path is not a work tree or declares no origin.
|
|
3029
|
+
* @param {string} repoPath - Local repository path.
|
|
3030
|
+
* @returns {string|null}
|
|
3031
|
+
* @memberof ServerConfBuilder
|
|
3032
|
+
*/
|
|
3033
|
+
const gitOriginRepositoryName = (repoPath) => {
|
|
3034
|
+
if (!fs.existsSync(`${repoPath}/.git`)) return null;
|
|
3035
|
+
const url = shellExec(`cd ${repoPath} && git config --get remote.origin.url`, {
|
|
3036
|
+
stdout: true,
|
|
3037
|
+
silent: true,
|
|
3038
|
+
disableLog: true,
|
|
3039
|
+
silentOnError: true,
|
|
3040
|
+
});
|
|
3041
|
+
const name = typeof url === 'string' ? url.trim().split('/').pop() : '';
|
|
3042
|
+
return name ? name.replace(/\.git$/, '') : null;
|
|
3043
|
+
};
|
|
3044
|
+
|
|
3045
|
+
/**
|
|
3046
|
+
* @method ensureTemplateCheckout
|
|
3047
|
+
* @description Guarantees `toPath` is a checkout of the template repository, cloning it
|
|
3048
|
+
* when it is missing or belongs to another repository.
|
|
3049
|
+
*
|
|
3050
|
+
* Publishing steps swap the checkout's `.git` for a product repository's history, so an
|
|
3051
|
+
* assembly that trusted whatever `.git` it found would read another product's identity
|
|
3052
|
+
* back into the base template `package.json`.
|
|
3053
|
+
*
|
|
3054
|
+
* @param {object} params
|
|
3055
|
+
* @param {string} params.toPath - Template checkout path.
|
|
3056
|
+
* @param {string} [params.githubUsername] - Owner of the template repository.
|
|
3057
|
+
* @param {boolean} [params.noClone=false] - Fail instead of cloning a missing/foreign checkout.
|
|
3058
|
+
* @returns {boolean} `true` when the checkout was restored.
|
|
3059
|
+
* @memberof ServerConfBuilder
|
|
3060
|
+
*/
|
|
3061
|
+
const ensureTemplateCheckout = ({ toPath, githubUsername, noClone = false }) => {
|
|
3062
|
+
const owner = githubUsername || 'underpostnet';
|
|
3063
|
+
const repository = `${owner}/${TEMPLATE_REPOSITORY_NAME}`;
|
|
3064
|
+
if (gitOriginRepositoryName(toPath) === TEMPLATE_REPOSITORY_NAME) return false;
|
|
3065
|
+
if (noClone) throw new Error(`buildTemplate: ${toPath} is not a ${repository} checkout (--no-clone)`);
|
|
3066
|
+
logger.warn('Restoring template checkout', { toPath, repository });
|
|
3067
|
+
|
|
3068
|
+
// Swap in the clone only once it is complete, so a failed one leaves the checkout as is.
|
|
3069
|
+
const templatePath = dir.resolve(toPath);
|
|
3070
|
+
const stagePath = fs.mkdtempSync(`${dir.dirname(templatePath)}/.${TEMPLATE_REPOSITORY_NAME}-`);
|
|
3071
|
+
try {
|
|
3072
|
+
shellExec(`cd ${stagePath} && node ${dir.resolve('./bin')} clone ${repository}`);
|
|
3073
|
+
const clonePath = `${stagePath}/${TEMPLATE_REPOSITORY_NAME}`;
|
|
3074
|
+
if (gitOriginRepositoryName(clonePath) !== TEMPLATE_REPOSITORY_NAME)
|
|
3075
|
+
throw new Error(`buildTemplate: could not clone ${repository}`);
|
|
3076
|
+
fs.removeSync(templatePath);
|
|
3077
|
+
fs.moveSync(clonePath, templatePath);
|
|
3078
|
+
} finally {
|
|
3079
|
+
fs.removeSync(stagePath);
|
|
3080
|
+
}
|
|
3081
|
+
return true;
|
|
3082
|
+
};
|
|
3083
|
+
|
|
3084
|
+
/**
|
|
3085
|
+
* @method pruneTemplateWorkTree
|
|
3086
|
+
* @description Empties the template checkout of everything a build reconstructs, so a
|
|
3087
|
+
* source renamed or deleted in the engine — or a client bundle belonging to another
|
|
3088
|
+
* deploy id — cannot survive from a previous run into the assembled output.
|
|
3089
|
+
* @param {string} toPath - Template checkout path.
|
|
3090
|
+
* @param {string[]} preservedEntries - Top level entries to keep (git history, installed modules).
|
|
3091
|
+
* @returns {void}
|
|
3092
|
+
* @memberof ServerConfBuilder
|
|
3093
|
+
*/
|
|
3094
|
+
const pruneTemplateWorkTree = (toPath, preservedEntries = []) => {
|
|
3095
|
+
for (const entry of fs.readdirSync(toPath)) {
|
|
3096
|
+
if (preservedEntries.includes(entry)) continue;
|
|
3097
|
+
fs.removeSync(`${toPath}/${entry}`);
|
|
3098
|
+
}
|
|
3099
|
+
};
|
|
3100
|
+
|
|
3034
3101
|
/**
|
|
3035
3102
|
* Rebuilds the standalone `pwa-microservices-template` from scratch out of the current
|
|
3036
3103
|
* engine source tree.
|
|
3037
3104
|
*
|
|
3038
|
-
* Clones the template repo next to the engine when
|
|
3039
|
-
*
|
|
3105
|
+
* Clones the template repo next to the engine when the checkout is missing or belongs to
|
|
3106
|
+
* another repository, empties it so the result cannot inherit anything from a previous run,
|
|
3107
|
+
* then syncs every engine-tracked file the template is allowed to carry
|
|
3040
3108
|
* ({@link validateTemplatePath}), strips engine-only + product modules, restores the template's
|
|
3041
3109
|
* own CI workflows + guest services, and rewrites `package.json` / `package-lock.json` / `README`
|
|
3042
3110
|
* so the result is a standalone, installable project. Throws on failure; callers own exit codes.
|
|
3043
3111
|
*
|
|
3044
|
-
* Product catalogs are read dynamically ({@link module:src/server/catalog} `loadProductCatalogs`),
|
|
3112
|
+
* Product catalogs are read dynamically ({@link module:src/server/build/catalog.js} `loadProductCatalogs`),
|
|
3045
3113
|
* so this stays decoupled from — and survives removal of — any product module.
|
|
3046
3114
|
*
|
|
3047
3115
|
* @method buildTemplate
|
|
3048
3116
|
* @param {object} [options]
|
|
3049
3117
|
* @param {string} [options.srcPath='./'] - Engine source root to sync from.
|
|
3050
3118
|
* @param {string} [options.toPath='../pwa-microservices-template'] - Template output path.
|
|
3119
|
+
* @param {boolean} [options.noClone=false] - Fail instead of cloning when the checkout is missing or foreign.
|
|
3051
3120
|
* @returns {Promise<void>}
|
|
3052
3121
|
* @memberof ServerConfBuilder
|
|
3053
3122
|
*/
|
|
3054
|
-
const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-template' } = {}) => {
|
|
3123
|
+
const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-template', noClone = false } = {}) => {
|
|
3055
3124
|
const walk = (await import('ignore-walk')).default;
|
|
3056
|
-
const { TEMPLATE_RESTORE_PATHS, TEMPLATE_KEYWORDS, TEMPLATE_DESCRIPTION } =
|
|
3057
|
-
await import('
|
|
3058
|
-
const { loadProductCatalogs } = await import('
|
|
3125
|
+
const { TEMPLATE_RESTORE_PATHS, TEMPLATE_PRESERVED_ENTRIES, TEMPLATE_KEYWORDS, TEMPLATE_DESCRIPTION } =
|
|
3126
|
+
await import('../../projects/underpost/catalog-underpost.js');
|
|
3127
|
+
const { loadProductCatalogs } = await import('../build/catalog.js');
|
|
3059
3128
|
const githubUsername = process.env.GITHUB_USERNAME;
|
|
3060
3129
|
|
|
3061
3130
|
logger.info('Build template', { srcPath, toPath });
|
|
@@ -3068,10 +3137,9 @@ const buildTemplate = async ({ srcPath = './', toPath = '../pwa-microservices-te
|
|
|
3068
3137
|
)
|
|
3069
3138
|
).filter((p) => !p.startsWith('.git'));
|
|
3070
3139
|
|
|
3071
|
-
|
|
3072
|
-
shellExec(`cd .. && node engine/bin clone ${githubUsername}/pwa-microservices-template`);
|
|
3073
|
-
|
|
3140
|
+
ensureTemplateCheckout({ toPath, githubUsername, noClone });
|
|
3074
3141
|
shellExec(`cd ${toPath} && git config core.filemode false`);
|
|
3142
|
+
pruneTemplateWorkTree(toPath, TEMPLATE_PRESERVED_ENTRIES);
|
|
3075
3143
|
|
|
3076
3144
|
for (const copyPath of sourceFiles) {
|
|
3077
3145
|
if (copyPath === 'NaN') continue;
|
|
@@ -3180,7 +3248,8 @@ git add .`);
|
|
|
3180
3248
|
* `engine-<idPart>`). A pod started with `underpost start --build --private-test-repo`
|
|
3181
3249
|
* clones this repo, so work-in-progress engine source can be tested end to end
|
|
3182
3250
|
* without touching the production source. Mirrors {@link updatePrivateTemplateRepo}
|
|
3183
|
-
* but per-deploy-id and against the test repo
|
|
3251
|
+
* but per-deploy-id and against the test repo, publishing from its own work tree
|
|
3252
|
+
* (`<template parent>/engine-test-<idPart>`) so the template checkout keeps its identity.
|
|
3184
3253
|
*
|
|
3185
3254
|
* Assumes the deploy id template has already been assembled at the template path
|
|
3186
3255
|
* (run `node bin/build <deployId>` first, or use `node bin/build <deployId> --update-private`).
|
|
@@ -3190,39 +3259,53 @@ git add .`);
|
|
|
3190
3259
|
*/
|
|
3191
3260
|
const updatePrivateEngineTestRepo = async (deployId) => {
|
|
3192
3261
|
const username = process.env.GITHUB_USERNAME || 'underpostnet';
|
|
3193
|
-
const repoName =
|
|
3262
|
+
const repoName = Underpost.repo.engineRepoFactory(deployId, { test: true });
|
|
3194
3263
|
const templatePath = '/home/dd/pwa-microservices-template';
|
|
3195
3264
|
if (!fs.existsSync(templatePath))
|
|
3196
3265
|
throw new Error(`updatePrivateEngineTestRepo: assemble the template first (node bin/build ${deployId})`);
|
|
3197
3266
|
|
|
3198
|
-
// Detach the assembled working tree from any engine-build git history.
|
|
3199
|
-
shellExec(`sudo rm -rf ${templatePath}/.git`);
|
|
3200
|
-
|
|
3201
3267
|
// Adopt the test repo's existing history when present (so the push is a delta);
|
|
3202
3268
|
// otherwise publish a fresh history on first push.
|
|
3203
|
-
|
|
3269
|
+
const parentPath = dir.dirname(templatePath);
|
|
3270
|
+
shellExec(`cd ${parentPath} && sudo rm -rf ./${repoName}.git && underpost clone --bare ${username}/${repoName}`, {
|
|
3204
3271
|
silent: true,
|
|
3205
3272
|
disableLog: true,
|
|
3206
3273
|
silentOnError: true,
|
|
3207
3274
|
});
|
|
3208
|
-
|
|
3275
|
+
|
|
3276
|
+
// Publishing from a work tree of its own leaves the template checkout its own git
|
|
3277
|
+
// history, and copying that tree from 0 keeps a stale file out of the published one.
|
|
3278
|
+
const publishPath = `${parentPath}/${repoName}`;
|
|
3279
|
+
shellExec(`sudo rm -rf ${publishPath}`);
|
|
3280
|
+
fs.copySync(templatePath, publishPath, {
|
|
3281
|
+
filter: (src) => {
|
|
3282
|
+
const entries = dir.relative(templatePath, src).split(dir.sep);
|
|
3283
|
+
return !entries.includes('.git') && !entries.includes('node_modules');
|
|
3284
|
+
},
|
|
3285
|
+
});
|
|
3286
|
+
if (fs.existsSync(`${parentPath}/${repoName}.git`)) shellExec(`mv ${parentPath}/${repoName}.git ${publishPath}/.git`);
|
|
3209
3287
|
|
|
3210
3288
|
// `git init` converts the moved bare repo into a normal work-tree repo (bare
|
|
3211
3289
|
// clones have no work tree, so `git add` would fail), and bootstraps a fresh
|
|
3212
3290
|
// repo on first publish. Idempotent — mirrors updatePrivateTemplateRepo.
|
|
3213
|
-
shellExec(`cd ${
|
|
3291
|
+
shellExec(`cd ${publishPath}
|
|
3214
3292
|
git init
|
|
3215
3293
|
git config user.name '${username}'
|
|
3216
3294
|
git config user.email 'development@underpost.net'
|
|
3217
|
-
git add .`);
|
|
3295
|
+
git add -A .`);
|
|
3218
3296
|
|
|
3219
|
-
|
|
3297
|
+
// The bundled coverage report is a build output the artifact has to carry: force-added so a
|
|
3298
|
+
// tree-level ignore rule can never silently drop it from the published source.
|
|
3299
|
+
if (fs.existsSync(`${publishPath}/${COVERAGE_BUNDLE_DIRECTORY}`))
|
|
3300
|
+
shellExec(`cd ${publishPath} && git add -A -f ${COVERAGE_BUNDLE_DIRECTORY}`);
|
|
3301
|
+
|
|
3302
|
+
const hasChanges = shellExec(`node bin cmt ${publishPath} --has-changes`, {
|
|
3220
3303
|
stdout: true,
|
|
3221
3304
|
silent: true,
|
|
3222
3305
|
disableLog: true,
|
|
3223
3306
|
}).trim();
|
|
3224
3307
|
if (hasChanges === '1')
|
|
3225
|
-
shellExec(`cd ${
|
|
3308
|
+
shellExec(`cd ${publishPath} && git commit -m 'Update ${repoName}' && underpost push . ${username}/${repoName}`);
|
|
3226
3309
|
else logger.info('No changes to publish', { repoName });
|
|
3227
3310
|
};
|
|
3228
3311
|
|
|
@@ -3243,12 +3326,15 @@ git add .`);
|
|
|
3243
3326
|
* instance runs — `/etc/hosts` is written in one pass for every host the
|
|
3244
3327
|
* gateway will serve.
|
|
3245
3328
|
* @param {Array<string>} deployList - Deploy ids being brought up.
|
|
3246
|
-
* @param {string} [instanceList] -
|
|
3329
|
+
* @param {string} [instanceList] - Comma-separated instance/template ids, as `--instance-id` takes them.
|
|
3247
3330
|
* @returns {{ byDeployId: Object<string,{ids: Array<string>, hosts: Array<string>}>, unmatched: Array<string> }}
|
|
3248
3331
|
* Per-deploy selection, and the requested ids no deploy declares.
|
|
3249
3332
|
*/
|
|
3250
3333
|
const clusterInstancesFactory = (deployList = [], instanceList = '') => {
|
|
3251
|
-
const requested = `${instanceList || ''}
|
|
3334
|
+
const requested = `${instanceList || ''}`
|
|
3335
|
+
.split(',')
|
|
3336
|
+
.map((id) => id.trim())
|
|
3337
|
+
.filter(Boolean);
|
|
3252
3338
|
const byDeployId = Object.fromEntries(
|
|
3253
3339
|
deployList.map((deployId) => {
|
|
3254
3340
|
const confPath = `./engine-private/conf/${deployId}/conf.instances.json`;
|
|
@@ -3273,6 +3359,10 @@ const clusterInstancesFactory = (deployList = [], instanceList = '') => {
|
|
|
3273
3359
|
};
|
|
3274
3360
|
|
|
3275
3361
|
export {
|
|
3362
|
+
cleanDeployEnvFiles,
|
|
3363
|
+
deployEnvFilePath,
|
|
3364
|
+
deployEnvNamesFactory,
|
|
3365
|
+
instanceEnvFilePath,
|
|
3276
3366
|
Config,
|
|
3277
3367
|
loadConf,
|
|
3278
3368
|
loadConfInstances,
|
|
@@ -3292,25 +3382,19 @@ export {
|
|
|
3292
3382
|
addWsConf,
|
|
3293
3383
|
buildWsSrc,
|
|
3294
3384
|
cloneSrcComponents,
|
|
3295
|
-
buildProxyRouter,
|
|
3296
3385
|
getDataDeploy,
|
|
3297
3386
|
validateTemplatePath,
|
|
3298
3387
|
buildReplicaId,
|
|
3299
3388
|
mergeFile,
|
|
3300
3389
|
getPathsSSR,
|
|
3301
|
-
buildKindPorts,
|
|
3302
|
-
buildPortProxyRouter,
|
|
3303
3390
|
splitFileFactory,
|
|
3304
3391
|
generateSecurePassword,
|
|
3305
3392
|
resolveReplicaCount,
|
|
3306
|
-
pathPortAssignmentFactory,
|
|
3307
|
-
deployRangePortFactory,
|
|
3308
3393
|
awaitDeployMonitor,
|
|
3309
3394
|
buildCliDoc,
|
|
3310
3395
|
getInstanceContext,
|
|
3311
3396
|
buildApiConf,
|
|
3312
3397
|
buildClientStaticConf,
|
|
3313
|
-
isDeployRunnerContext,
|
|
3314
3398
|
isDevProxyContext,
|
|
3315
3399
|
devProxyHostFactory,
|
|
3316
3400
|
isTlsDevProxy,
|
|
@@ -3320,6 +3404,10 @@ export {
|
|
|
3320
3404
|
loadConfServerJson,
|
|
3321
3405
|
getConfFolder,
|
|
3322
3406
|
getConfFilePath,
|
|
3407
|
+
deployEnvContentFactory,
|
|
3408
|
+
deployOciEnvFilePath,
|
|
3409
|
+
ociEnvContentFactory,
|
|
3410
|
+
OCI_ENV_SUFFIX,
|
|
3323
3411
|
readConfJson,
|
|
3324
3412
|
DEFAULT_DEPLOY_ID,
|
|
3325
3413
|
clusterContextFactory,
|
|
@@ -3344,10 +3432,13 @@ export {
|
|
|
3344
3432
|
trafficProbePathsFactory,
|
|
3345
3433
|
hostIngressFactsFactory,
|
|
3346
3434
|
curlStatusChainFactory,
|
|
3435
|
+
publicIngressProbeFactory,
|
|
3436
|
+
publicIngressUrlsFactory,
|
|
3347
3437
|
hostRenderInstancesFactory,
|
|
3348
3438
|
instanceTrafficPlanFactory,
|
|
3349
3439
|
isTrafficServingFactory,
|
|
3350
3440
|
nextTrafficFactory,
|
|
3441
|
+
redeployPlanFactory,
|
|
3351
3442
|
schedulableNodeFactory,
|
|
3352
3443
|
stopPlanFactory,
|
|
3353
3444
|
trafficFromRoutingInfoFactory,
|
|
@@ -3355,9 +3446,11 @@ export {
|
|
|
3355
3446
|
resolveEnvScoped,
|
|
3356
3447
|
sortInstancesByPath,
|
|
3357
3448
|
waitForPort,
|
|
3358
|
-
resolveDeployList,
|
|
3359
3449
|
syncPrivateConf,
|
|
3360
3450
|
syncDeployIdSources,
|
|
3451
|
+
gitOriginRepositoryName,
|
|
3452
|
+
ensureTemplateCheckout,
|
|
3453
|
+
pruneTemplateWorkTree,
|
|
3361
3454
|
buildTemplate,
|
|
3362
3455
|
updatePrivateTemplateRepo,
|
|
3363
3456
|
updatePrivateEngineTestRepo,
|