@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
|
@@ -0,0 +1,1187 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { expect } from 'chai';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import UnderpostWireguard, {
|
|
7
|
+
EDGE_TOPOLOGY_PATH,
|
|
8
|
+
UNDERPOST_EDGE,
|
|
9
|
+
hostAddressesFactory,
|
|
10
|
+
readEdgeContext,
|
|
11
|
+
readNodeConfigs,
|
|
12
|
+
readTopology,
|
|
13
|
+
} from '../../../../src/cli/wireguard.js';
|
|
14
|
+
import Underpost from '../../../../src/index.js';
|
|
15
|
+
import UnderpostEvent from '../../../../src/cli/event.js';
|
|
16
|
+
import UnderpostRepository from '../../../../src/cli/repository.js';
|
|
17
|
+
import { FORWARD_PROXY } from '../../../../src/server/network/forward-proxy.js';
|
|
18
|
+
import { shellHarness } from '../../../support/shell-harness.js';
|
|
19
|
+
|
|
20
|
+
// Emitted only when this checkout ships the script it names, so the case needs a tree that
|
|
21
|
+
// carries the deploy: the base template restores no deploy id of its own.
|
|
22
|
+
const shipsCyberiaPackageScript = fs.existsSync('./deploy/dd-cyberia/package.sh');
|
|
23
|
+
|
|
24
|
+
const NODES_PATH = './engine-private/deploy/nodes';
|
|
25
|
+
const ROUTES_PATH = './engine-private/deploy/dd.routes';
|
|
26
|
+
|
|
27
|
+
const HUB_HOST = '203.0.113.10';
|
|
28
|
+
const TOPOLOGY = {
|
|
29
|
+
[HUB_HOST]: {
|
|
30
|
+
interfaceName: 'wg0',
|
|
31
|
+
listenPort: 51820,
|
|
32
|
+
address: '10.0.0.1/24',
|
|
33
|
+
publicKey: 'hubPublicKeyFixture=',
|
|
34
|
+
sshForwardPort: 2222,
|
|
35
|
+
peers: [
|
|
36
|
+
{
|
|
37
|
+
id: 'control-a',
|
|
38
|
+
address: '10.0.0.2',
|
|
39
|
+
managementHost: '198.51.100.2',
|
|
40
|
+
publicKey: 'peerAPublicKeyFixture=',
|
|
41
|
+
allowedIPs: ['10.0.0.2/32'],
|
|
42
|
+
hosts: ['app.fixture.test'],
|
|
43
|
+
instances: [],
|
|
44
|
+
default: true,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'worker-b',
|
|
48
|
+
address: '10.0.0.3',
|
|
49
|
+
managementHost: '198.51.100.3',
|
|
50
|
+
publicKey: 'peerBPublicKeyFixture=',
|
|
51
|
+
allowedIPs: ['10.0.0.3/32'],
|
|
52
|
+
hosts: [],
|
|
53
|
+
instances: ['mmo-server'],
|
|
54
|
+
default: false,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// The edge reads its topology and node documents out of the private repository
|
|
61
|
+
// and reconciles /etc, iptables and systemd from them. Both are replaced here:
|
|
62
|
+
// an in-memory tree for the documents, and the shell harness for the host, so
|
|
63
|
+
// what is asserted is the command vector and the files the run would install.
|
|
64
|
+
const edgeFixture = ({ files = {}, hostname = 'hub-node' } = {}) => {
|
|
65
|
+
const table = new Map(Object.entries(files));
|
|
66
|
+
const written = new Map();
|
|
67
|
+
const removedDirectories = [];
|
|
68
|
+
|
|
69
|
+
const read = (filePath) => {
|
|
70
|
+
const key = `${filePath}`;
|
|
71
|
+
if (written.has(key)) return written.get(key);
|
|
72
|
+
if (table.has(key)) return table.get(key);
|
|
73
|
+
throw Object.assign(new Error(`ENOENT: ${key}`), { code: 'ENOENT' });
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
vi.spyOn(os, 'hostname').mockReturnValue(hostname);
|
|
77
|
+
vi.spyOn(fs, 'existsSync').mockImplementation((filePath) => {
|
|
78
|
+
const key = `${filePath}`;
|
|
79
|
+
if (table.has(key) || written.has(key)) return true;
|
|
80
|
+
return [...table.keys(), ...written.keys()].some((entry) => entry.startsWith(`${key}/`));
|
|
81
|
+
});
|
|
82
|
+
vi.spyOn(fs, 'readFileSync').mockImplementation(read);
|
|
83
|
+
vi.spyOn(fs, 'writeFileSync').mockImplementation((filePath, value) => written.set(`${filePath}`, `${value}`));
|
|
84
|
+
vi.spyOn(fs, 'mkdirpSync').mockImplementation((dir) => removedDirectories.push(`${dir}`));
|
|
85
|
+
vi.spyOn(fs, 'removeSync').mockImplementation(() => undefined);
|
|
86
|
+
vi.spyOn(fs, 'readdirSync').mockImplementation((dir) =>
|
|
87
|
+
[...table.keys(), ...written.keys()]
|
|
88
|
+
.filter((filePath) => filePath.startsWith(`${dir}/`))
|
|
89
|
+
.map((filePath) => filePath.slice(`${dir}/`.length))
|
|
90
|
+
.filter((name) => !name.includes('/')),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return { table, written };
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const nodeDocument = (identity) => JSON.stringify(identity);
|
|
97
|
+
|
|
98
|
+
const HUB_IDENTITY_FILES = {
|
|
99
|
+
[EDGE_TOPOLOGY_PATH]: JSON.stringify(TOPOLOGY),
|
|
100
|
+
[`${NODES_PATH}/hub-node.json`]: nodeDocument({ role: 'hub', hubHost: HUB_HOST }),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const SPOKE_IDENTITY_FILES = {
|
|
104
|
+
[EDGE_TOPOLOGY_PATH]: JSON.stringify(TOPOLOGY),
|
|
105
|
+
[`${NODES_PATH}/control-node.json`]: nodeDocument({ role: 'control', hubHost: HUB_HOST, peerId: 'control-a' }),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
describe('edge topology and node documents', () => {
|
|
109
|
+
afterEach(() => vi.restoreAllMocks());
|
|
110
|
+
|
|
111
|
+
it('reads no topology at all when the private repository is not checked out', () => {
|
|
112
|
+
edgeFixture();
|
|
113
|
+
expect(readTopology()).to.deep.equal({});
|
|
114
|
+
expect(readNodeConfigs()).to.deep.equal([]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('normalizes a hand-edited topology into the canonical shape', () => {
|
|
118
|
+
edgeFixture({ files: { [EDGE_TOPOLOGY_PATH]: JSON.stringify({ [HUB_HOST]: { peers: [{ id: 'a' }, {}] } }) } });
|
|
119
|
+
const hub = readTopology()[HUB_HOST];
|
|
120
|
+
expect(hub.interfaceName).to.equal(UNDERPOST_EDGE.interfaceName);
|
|
121
|
+
expect(hub.listenPort).to.equal(UNDERPOST_EDGE.listenPort);
|
|
122
|
+
expect(hub.address).to.equal(UNDERPOST_EDGE.cidr);
|
|
123
|
+
// An entry with no id names no peer, so it is dropped rather than written
|
|
124
|
+
// back as a nameless one.
|
|
125
|
+
expect(hub.peers.map((peer) => peer.id)).to.deep.equal(['a']);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// A hostname key would make the hub's endpoint depend on a resolver the
|
|
129
|
+
// spokes reach it before they can query.
|
|
130
|
+
it('refuses a topology keyed by anything but a static IPv4 address', () => {
|
|
131
|
+
edgeFixture({ files: { [EDGE_TOPOLOGY_PATH]: JSON.stringify({ 'hub.fixture.test': {} }) } });
|
|
132
|
+
expect(() => readTopology()).to.throw('must be static IPv4 addresses');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('names the file rather than the parser when topology is malformed', () => {
|
|
136
|
+
edgeFixture({ files: { [EDGE_TOPOLOGY_PATH]: '{ not json' } });
|
|
137
|
+
expect(() => readTopology()).to.throw('invalid topology');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('resolves this machine to its node document and the hub it belongs to', () => {
|
|
141
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
142
|
+
expect(readEdgeContext()).to.include({
|
|
143
|
+
nodeName: 'hub-node',
|
|
144
|
+
role: 'hub',
|
|
145
|
+
hubHost: HUB_HOST,
|
|
146
|
+
address: '10.0.0.1/24',
|
|
147
|
+
publicKey: 'hubPublicKeyFixture=',
|
|
148
|
+
endpoint: `${HUB_HOST}:51820`,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('resolves a spoke to its own peer entry and the hub key it dials', () => {
|
|
153
|
+
edgeFixture({ files: SPOKE_IDENTITY_FILES, hostname: 'control-node' });
|
|
154
|
+
expect(readEdgeContext()).to.include({
|
|
155
|
+
role: 'control',
|
|
156
|
+
peerId: 'control-a',
|
|
157
|
+
address: '10.0.0.2',
|
|
158
|
+
publicKey: 'peerAPublicKeyFixture=',
|
|
159
|
+
hubPublicKey: 'hubPublicKeyFixture=',
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// An FQDN is a property of the resolver domain, not of the node.
|
|
164
|
+
it('matches a node document recorded under the short hostname', () => {
|
|
165
|
+
edgeFixture({ files: HUB_IDENTITY_FILES, hostname: 'hub-node.guest' });
|
|
166
|
+
expect(readEdgeContext().nodeName).to.equal('hub-node');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('names the command that registers a host with no identity', () => {
|
|
170
|
+
edgeFixture({ files: { [EDGE_TOPOLOGY_PATH]: JSON.stringify(TOPOLOGY) } });
|
|
171
|
+
expect(() => readEdgeContext()).to.throw('--node-config');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('refuses an identity pointing at a hub the topology never registered', () => {
|
|
175
|
+
edgeFixture({ files: { [`${NODES_PATH}/hub-node.json`]: nodeDocument({ role: 'hub', hubHost: HUB_HOST }) } });
|
|
176
|
+
expect(() => readEdgeContext()).to.throw('is not registered');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('refuses an identity referencing a peer the hub does not carry', () => {
|
|
180
|
+
edgeFixture({
|
|
181
|
+
files: {
|
|
182
|
+
[EDGE_TOPOLOGY_PATH]: JSON.stringify(TOPOLOGY),
|
|
183
|
+
[`${NODES_PATH}/ghost.json`]: nodeDocument({ role: 'worker', hubHost: HUB_HOST, peerId: 'not-a-peer' }),
|
|
184
|
+
},
|
|
185
|
+
hostname: 'ghost',
|
|
186
|
+
});
|
|
187
|
+
expect(() => readEdgeContext()).to.throw('unknown peer');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('reads the routable addresses of this machine', () => {
|
|
191
|
+
vi.spyOn(os, 'networkInterfaces').mockReturnValue({
|
|
192
|
+
lo: [{ family: 'IPv4', address: '127.0.0.1', internal: true }],
|
|
193
|
+
eth0: [
|
|
194
|
+
{ family: 'IPv4', address: '198.51.100.2', internal: false },
|
|
195
|
+
{ family: 'IPv6', address: '::1' },
|
|
196
|
+
],
|
|
197
|
+
});
|
|
198
|
+
expect([...hostAddressesFactory()]).to.deep.equal(['198.51.100.2']);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('edge host provisioning', () => {
|
|
203
|
+
let harness;
|
|
204
|
+
|
|
205
|
+
beforeEach(() => {
|
|
206
|
+
harness = shellHarness();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
afterEach(() => {
|
|
210
|
+
harness.restore();
|
|
211
|
+
vi.restoreAllMocks();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('install', () => {
|
|
215
|
+
it('refuses a host the subsystem does not target', () => {
|
|
216
|
+
harness.route({ match: 'command -v dnf', code: 1, stdout: '' });
|
|
217
|
+
expect(() => UnderpostWireguard.API.install()).to.throw('targets RHEL 9');
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('installs only the packages the host is missing', () => {
|
|
221
|
+
harness.route({ match: 'rpm -q', code: 1 });
|
|
222
|
+
harness.route({ match: 'rpm -q wireguard-tools', code: 0 });
|
|
223
|
+
harness.route({ match: 'command -v dnf', code: 0, stdout: '/usr/bin/dnf\n' });
|
|
224
|
+
expect(UnderpostWireguard.API.install()).to.deep.equal(['haproxy', 'iptables']);
|
|
225
|
+
expect(harness.ran('dnf -y install haproxy iptables')).to.equal(true);
|
|
226
|
+
// HAProxy dials backends over the tunnel; without the boolean SELinux
|
|
227
|
+
// refuses the connection and every route answers 503.
|
|
228
|
+
expect(harness.ran('setsebool -P haproxy_connect_any 1')).to.equal(true);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('does no work on an already provisioned host', () => {
|
|
232
|
+
harness.route({ match: 'command -v dnf', code: 0, stdout: '/usr/bin/dnf\n' });
|
|
233
|
+
harness.route({ match: 'rpm -q', code: 0 });
|
|
234
|
+
expect(UnderpostWireguard.API.install()).to.deep.equal([]);
|
|
235
|
+
expect(harness.ran('dnf -y install')).to.equal(false);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('key pair', () => {
|
|
240
|
+
it('generates the private half under a restrictive umask and never reads it back', () => {
|
|
241
|
+
harness.route({ match: 'sudo test -s', code: 1 });
|
|
242
|
+
harness.route({ match: 'sudo cat', code: 0, stdout: 'generatedPublicKey=\n' });
|
|
243
|
+
const result = UnderpostWireguard.API.ensureKeyPair('wg0');
|
|
244
|
+
expect(result).to.include({ publicKey: 'generatedPublicKey=', generated: true });
|
|
245
|
+
expect(harness.ran('umask 077 && mkdir -p /etc/wireguard && wg genkey')).to.equal(true);
|
|
246
|
+
expect(harness.ran('chmod 0600 /etc/wireguard/wg0.key')).to.equal(true);
|
|
247
|
+
expect(harness.ran('cat /etc/wireguard/wg0.key')).to.equal(false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('reuses an existing key pair', () => {
|
|
251
|
+
harness.route({ match: 'sudo test -s', code: 0 });
|
|
252
|
+
harness.route({ match: 'sudo cat', code: 0, stdout: 'existingPublicKey=\n' });
|
|
253
|
+
expect(UnderpostWireguard.API.ensureKeyPair('wg0')).to.include({
|
|
254
|
+
publicKey: 'existingPublicKey=',
|
|
255
|
+
generated: false,
|
|
256
|
+
});
|
|
257
|
+
expect(harness.ran('wg genkey')).to.equal(false);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('generates nothing under a dry run', () => {
|
|
261
|
+
harness.route({ match: 'sudo test -s', code: 1 });
|
|
262
|
+
expect(UnderpostWireguard.API.ensureKeyPair('wg0', true)).to.include({ publicKey: '', generated: true });
|
|
263
|
+
expect(harness.ran('wg genkey')).to.equal(false);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('node identity', () => {
|
|
268
|
+
it('records the node document for a hub', () => {
|
|
269
|
+
const { written } = edgeFixture({ files: { [EDGE_TOPOLOGY_PATH]: JSON.stringify(TOPOLOGY) } });
|
|
270
|
+
const saved = UnderpostWireguard.API.nodeConfig({ nodeRole: 'hub', hubHost: HUB_HOST });
|
|
271
|
+
expect(saved).to.include({ nodeName: 'hub-node', role: 'hub', hubHost: HUB_HOST });
|
|
272
|
+
expect(JSON.parse(written.get(`${NODES_PATH}/hub-node.json`))).to.deep.equal({
|
|
273
|
+
role: 'hub',
|
|
274
|
+
hubHost: HUB_HOST,
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('records the peer id a spoke is registered under', () => {
|
|
279
|
+
const { written } = edgeFixture({ files: { [EDGE_TOPOLOGY_PATH]: JSON.stringify(TOPOLOGY) } });
|
|
280
|
+
UnderpostWireguard.API.nodeConfig({ nodeRole: 'worker', hubHost: HUB_HOST, peerId: 'worker-b' });
|
|
281
|
+
expect(JSON.parse(written.get(`${NODES_PATH}/hub-node.json`)).peerId).to.equal('worker-b');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('writes nothing under a dry run', () => {
|
|
285
|
+
const { written } = edgeFixture({ files: { [EDGE_TOPOLOGY_PATH]: JSON.stringify(TOPOLOGY) } });
|
|
286
|
+
UnderpostWireguard.API.nodeConfig({ nodeRole: 'hub', hubHost: HUB_HOST, dryRun: true });
|
|
287
|
+
expect(written.size).to.equal(0);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('refuses an unknown role, a non-IPv4 hub, and an unregistered hub', () => {
|
|
291
|
+
edgeFixture({ files: { [EDGE_TOPOLOGY_PATH]: JSON.stringify(TOPOLOGY) } });
|
|
292
|
+
expect(() => UnderpostWireguard.API.nodeConfig({ nodeRole: 'edge', hubHost: HUB_HOST })).to.throw(
|
|
293
|
+
'--node-role must be one of',
|
|
294
|
+
);
|
|
295
|
+
expect(() => UnderpostWireguard.API.nodeConfig({ nodeRole: 'hub', hubHost: 'hub.fixture.test' })).to.throw(
|
|
296
|
+
'--hub-host must be the hub static IPv4 address',
|
|
297
|
+
);
|
|
298
|
+
expect(() => UnderpostWireguard.API.nodeConfig({ nodeRole: 'hub', hubHost: '198.51.100.9' })).to.throw(
|
|
299
|
+
'is not registered',
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('refuses a spoke with no peer id, or one the hub never registered', () => {
|
|
304
|
+
edgeFixture({ files: { [EDGE_TOPOLOGY_PATH]: JSON.stringify(TOPOLOGY) } });
|
|
305
|
+
expect(() => UnderpostWireguard.API.nodeConfig({ nodeRole: 'worker', hubHost: HUB_HOST })).to.throw(
|
|
306
|
+
'--peer-id is required',
|
|
307
|
+
);
|
|
308
|
+
expect(() =>
|
|
309
|
+
UnderpostWireguard.API.nodeConfig({ nodeRole: 'worker', hubHost: HUB_HOST, peerId: 'ghost' }),
|
|
310
|
+
).to.throw('add it with --peer-add first');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Two nodes claiming one identity is what makes a fleet command reach the
|
|
314
|
+
// wrong machine.
|
|
315
|
+
it('refuses an identity another node already holds', () => {
|
|
316
|
+
edgeFixture({
|
|
317
|
+
files: {
|
|
318
|
+
[EDGE_TOPOLOGY_PATH]: JSON.stringify(TOPOLOGY),
|
|
319
|
+
[`${NODES_PATH}/other.json`]: nodeDocument({ role: 'hub', hubHost: HUB_HOST }),
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
expect(() => UnderpostWireguard.API.nodeConfig({ nodeRole: 'hub', hubHost: HUB_HOST })).to.throw(
|
|
323
|
+
"is already assigned to node 'other'",
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
vi.restoreAllMocks();
|
|
327
|
+
edgeFixture({
|
|
328
|
+
files: {
|
|
329
|
+
[EDGE_TOPOLOGY_PATH]: JSON.stringify(TOPOLOGY),
|
|
330
|
+
[`${NODES_PATH}/other.json`]: nodeDocument({ role: 'worker', hubHost: HUB_HOST, peerId: 'worker-b' }),
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
expect(() =>
|
|
334
|
+
UnderpostWireguard.API.nodeConfig({ nodeRole: 'worker', hubHost: HUB_HOST, peerId: 'worker-b' }),
|
|
335
|
+
).to.throw("peer 'worker-b' is already assigned");
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('setup', () => {
|
|
340
|
+
const stubKeyPair = () => {
|
|
341
|
+
harness.route({ match: 'sudo test -s', code: 0 });
|
|
342
|
+
harness.route({ match: 'sudo cat', code: 0, stdout: 'hubPublicKeyFixture=\n' });
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
it('installs the hub interface config, forwarding and firewall rules', () => {
|
|
346
|
+
const { written } = edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
347
|
+
stubKeyPair();
|
|
348
|
+
const next = UnderpostWireguard.API.setup({});
|
|
349
|
+
expect(next.role).to.equal('hub');
|
|
350
|
+
expect(harness.ran('install -m 0600 -o root -g root')).to.equal(true);
|
|
351
|
+
expect(harness.ran('echo net.ipv4.ip_forward=1')).to.equal(true);
|
|
352
|
+
expect(harness.ran('sysctl -q --system')).to.equal(true);
|
|
353
|
+
expect(written.has(EDGE_TOPOLOGY_PATH)).to.equal(true);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('writes a client config that dials the hub endpoint on a spoke', () => {
|
|
357
|
+
const staged = [];
|
|
358
|
+
edgeFixture({ files: SPOKE_IDENTITY_FILES, hostname: 'control-node' });
|
|
359
|
+
vi.spyOn(fs, 'writeFileSync').mockImplementation((filePath, value) => staged.push(`${value}`));
|
|
360
|
+
harness.route({ match: 'sudo test -s', code: 0 });
|
|
361
|
+
harness.route({ match: 'sudo cat', code: 0, stdout: 'peerAPublicKeyFixture=\n' });
|
|
362
|
+
|
|
363
|
+
UnderpostWireguard.API.setup({});
|
|
364
|
+
|
|
365
|
+
const conf = staged.find((content) => content.includes('[Peer]'));
|
|
366
|
+
expect(conf).to.include(`Endpoint = ${HUB_HOST}:51820`);
|
|
367
|
+
expect(conf).to.include('PublicKey = hubPublicKeyFixture=');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('refuses hub-only flags on a spoke', () => {
|
|
371
|
+
edgeFixture({ files: SPOKE_IDENTITY_FILES, hostname: 'control-node' });
|
|
372
|
+
expect(() => UnderpostWireguard.API.setup({ port: 51821 })).to.throw('configure the hub only');
|
|
373
|
+
expect(() => UnderpostWireguard.API.setup({ sshForwardPort: 2222 })).to.throw('configure the hub only');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('refuses a hub address with no prefix length', () => {
|
|
377
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
378
|
+
stubKeyPair();
|
|
379
|
+
expect(() => UnderpostWireguard.API.setup({ cidr: '10.0.0.1' })).to.throw('must carry a prefix length');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('refuses to record a node with no public key', () => {
|
|
383
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
384
|
+
harness.route({ match: 'sudo test -s', code: 0 });
|
|
385
|
+
harness.route({ match: 'sudo cat', code: 0, stdout: '' });
|
|
386
|
+
expect(() => UnderpostWireguard.API.setup({})).to.throw('has no WireGuard public key');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// The config is written idempotently; the running interface is not, so a
|
|
390
|
+
// changed file under a live unit is a divergence that stays silent until the
|
|
391
|
+
// next reboot re-reads it.
|
|
392
|
+
it('reports that a live interface still runs the previous config', () => {
|
|
393
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
394
|
+
stubKeyPair();
|
|
395
|
+
harness.route({ match: 'is-active --quiet wg-quick@wg0', code: 0 });
|
|
396
|
+
expect(() => UnderpostWireguard.API.setup({})).not.to.throw();
|
|
397
|
+
expect(harness.ran('is-active --quiet wg-quick@wg0')).to.equal(true);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('touches no host under --build-conf', () => {
|
|
401
|
+
const { written } = edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
402
|
+
UnderpostWireguard.API.setup({ buildConf: true, publicKey: 'offHostKey=' });
|
|
403
|
+
expect(harness.calls.length).to.equal(0);
|
|
404
|
+
expect(written.get(EDGE_TOPOLOGY_PATH)).to.include('offHostKey=');
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
describe('peer registration', () => {
|
|
409
|
+
it('installs the peer on the live interface and records it', () => {
|
|
410
|
+
const { written } = edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
411
|
+
const hub = UnderpostWireguard.API.peerAdd({
|
|
412
|
+
peerAdd: 'worker-c',
|
|
413
|
+
peerIp: '10.0.0.4',
|
|
414
|
+
publicKey: 'peerCPublicKeyFixture=',
|
|
415
|
+
hosts: 'c.fixture.test',
|
|
416
|
+
managementHost: '198.51.100.4',
|
|
417
|
+
});
|
|
418
|
+
expect(hub.peers.map((peer) => peer.id)).to.deep.equal(['control-a', 'worker-b', 'worker-c']);
|
|
419
|
+
expect(harness.ran('wg set wg0 peer peerCPublicKeyFixture= allowed-ips 10.0.0.4/32')).to.equal(true);
|
|
420
|
+
expect(written.get(EDGE_TOPOLOGY_PATH)).to.include('worker-c');
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// WireGuard identifies a peer by its key, so admitting a new one does not
|
|
424
|
+
// replace the old: the superseded key would keep claiming the same AllowedIPs.
|
|
425
|
+
it('drops the superseded key when a spoke re-keys', () => {
|
|
426
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
427
|
+
UnderpostWireguard.API.peerAdd({
|
|
428
|
+
peerAdd: 'worker-b',
|
|
429
|
+
peerIp: '10.0.0.3',
|
|
430
|
+
publicKey: 'rekeyedPublicKey=',
|
|
431
|
+
});
|
|
432
|
+
expect(harness.ran('peer peerBPublicKeyFixture= remove')).to.equal(true);
|
|
433
|
+
expect(harness.ran('peer rekeyedPublicKey= allowed-ips')).to.equal(true);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('keeps the bindings a previous registration gave a re-keyed spoke', () => {
|
|
437
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
438
|
+
const hub = UnderpostWireguard.API.peerAdd({
|
|
439
|
+
peerAdd: 'worker-b',
|
|
440
|
+
peerIp: '10.0.0.3',
|
|
441
|
+
publicKey: 'rekeyedPublicKey=',
|
|
442
|
+
});
|
|
443
|
+
expect(hub.peers.find((peer) => peer.id === 'worker-b').instances).to.deep.equal(['mmo-server']);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('refuses a registration missing an id, an address or a key', () => {
|
|
447
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
448
|
+
expect(() => UnderpostWireguard.API.peerAdd({})).to.throw('requires a peer id');
|
|
449
|
+
expect(() => UnderpostWireguard.API.peerAdd({ peerAdd: 'x' })).to.throw('requires --peer-ip');
|
|
450
|
+
expect(() => UnderpostWireguard.API.peerAdd({ peerAdd: 'x', peerIp: '10.0.0.9' })).to.throw(
|
|
451
|
+
'requires --public-key',
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('refuses a live peer change from anywhere but the hub', () => {
|
|
456
|
+
edgeFixture({ files: SPOKE_IDENTITY_FILES, hostname: 'control-node' });
|
|
457
|
+
expect(() => UnderpostWireguard.API.peerAdd({ peerAdd: 'x', peerIp: '10.0.0.9', publicKey: 'k=' })).to.throw(
|
|
458
|
+
'use --build-conf off-host',
|
|
459
|
+
);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('refuses a hub the topology never registered', () => {
|
|
463
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
464
|
+
expect(() => UnderpostWireguard.API.peerAdd({ hubHost: '198.51.100.9', peerAdd: 'x' })).to.throw(
|
|
465
|
+
'is not registered',
|
|
466
|
+
);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('records a peer off-host without touching the interface', () => {
|
|
470
|
+
const { written } = edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
471
|
+
UnderpostWireguard.API.peerAdd({
|
|
472
|
+
buildConf: true,
|
|
473
|
+
hubHost: HUB_HOST,
|
|
474
|
+
peerAdd: 'worker-c',
|
|
475
|
+
peerIp: '10.0.0.4',
|
|
476
|
+
publicKey: 'k=',
|
|
477
|
+
});
|
|
478
|
+
expect(harness.calls.length).to.equal(0);
|
|
479
|
+
expect(written.get(EDGE_TOPOLOGY_PATH)).to.include('worker-c');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('removes a peer from the live interface and from topology', () => {
|
|
483
|
+
const { written } = edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
484
|
+
const hub = UnderpostWireguard.API.peerRemove({ peerRemove: 'worker-b' });
|
|
485
|
+
expect(hub.peers.map((peer) => peer.id)).to.deep.equal(['control-a']);
|
|
486
|
+
expect(harness.ran('peer peerBPublicKeyFixture= remove')).to.equal(true);
|
|
487
|
+
expect(written.get(EDGE_TOPOLOGY_PATH)).not.to.include('worker-b');
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('reports an unknown peer rather than rewriting the interface', () => {
|
|
491
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
492
|
+
expect(UnderpostWireguard.API.peerRemove({ peerRemove: 'ghost' }).peers.length).to.equal(2);
|
|
493
|
+
expect(harness.ran('wg set')).to.equal(false);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('refuses a live removal from a spoke', () => {
|
|
497
|
+
edgeFixture({ files: SPOKE_IDENTITY_FILES, hostname: 'control-node' });
|
|
498
|
+
expect(() => UnderpostWireguard.API.peerRemove({ peerRemove: 'worker-b' })).to.throw('--build-conf off-host');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('removes a peer off-host without touching the interface', () => {
|
|
502
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
503
|
+
UnderpostWireguard.API.peerRemove({ buildConf: true, hubHost: HUB_HOST, peerRemove: 'worker-b' });
|
|
504
|
+
expect(harness.calls.length).to.equal(0);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('normalizes topology in place, touching no host', () => {
|
|
508
|
+
const { written } = edgeFixture({
|
|
509
|
+
files: { [EDGE_TOPOLOGY_PATH]: JSON.stringify({ [HUB_HOST]: { peers: [{ id: 'a' }] } }) },
|
|
510
|
+
});
|
|
511
|
+
expect(Object.keys(UnderpostWireguard.API.buildConf())).to.deep.equal([HUB_HOST]);
|
|
512
|
+
expect(written.get(EDGE_TOPOLOGY_PATH)).to.include('"interfaceName": "wg0"');
|
|
513
|
+
expect(harness.calls.length).to.equal(0);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('reports the normalization without writing under a dry run', () => {
|
|
517
|
+
const { written } = edgeFixture({ files: { [EDGE_TOPOLOGY_PATH]: JSON.stringify(TOPOLOGY) } });
|
|
518
|
+
UnderpostWireguard.API.buildConf({ dryRun: true });
|
|
519
|
+
expect(written.size).to.equal(0);
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
describe('route table', () => {
|
|
524
|
+
const CONF_SERVER = {
|
|
525
|
+
'app.fixture.test': { '/': { client: 'App', proxy: [80, 443] } },
|
|
526
|
+
'other.fixture.test': { '/': { client: 'Other', proxy: [443] } },
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const routeFixture = (files = {}) =>
|
|
530
|
+
edgeFixture({
|
|
531
|
+
files: {
|
|
532
|
+
...HUB_IDENTITY_FILES,
|
|
533
|
+
[ROUTES_PATH]: 'dd-core',
|
|
534
|
+
'./engine-private/conf/dd-core/conf.server.json': JSON.stringify(CONF_SERVER),
|
|
535
|
+
...files,
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('resolves each published hostname to the spoke bound to it', () => {
|
|
540
|
+
routeFixture();
|
|
541
|
+
const table = UnderpostWireguard.API.routeTable('dd-core');
|
|
542
|
+
expect(table.deployList).to.deep.equal(['dd-core']);
|
|
543
|
+
expect(table.routes.find((route) => route.host === 'app.fixture.test').peerId).to.equal('control-a');
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('falls back to the whole route table when no deploy is named', () => {
|
|
547
|
+
routeFixture();
|
|
548
|
+
expect(UnderpostWireguard.API.routeTable().deployList).to.deep.equal(['dd-core']);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('skips a routed deploy whose configuration is not checked out', () => {
|
|
552
|
+
routeFixture({ [ROUTES_PATH]: 'dd-core,dd-absent' });
|
|
553
|
+
expect(UnderpostWireguard.API.routeTable('dd').missing).to.deep.equal(['dd-absent']);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('binds an instance host through the spoke that declares the instance', () => {
|
|
557
|
+
routeFixture({
|
|
558
|
+
'./engine-private/conf/dd-core/conf.instances.json': JSON.stringify([
|
|
559
|
+
{ id: 'mmo-server', host: 'game.fixture.test', path: '/' },
|
|
560
|
+
]),
|
|
561
|
+
});
|
|
562
|
+
const table = UnderpostWireguard.API.routeTable('dd-core');
|
|
563
|
+
expect(table.routes.find((route) => route.host === 'game.fixture.test').peerId).to.equal('worker-b');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('refuses a selector that names no deploy at all', () => {
|
|
567
|
+
routeFixture();
|
|
568
|
+
expect(() => UnderpostWireguard.API.routeTable(',,')).to.throw('resolved to no deploys');
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('refuses when no requested deploy has a readable configuration', () => {
|
|
572
|
+
routeFixture();
|
|
573
|
+
expect(() => UnderpostWireguard.API.routeTable('dd-absent')).to.throw('No readable deploy configuration');
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
describe('haproxy publication', () => {
|
|
578
|
+
const routeFixture = (files = {}) =>
|
|
579
|
+
edgeFixture({
|
|
580
|
+
files: {
|
|
581
|
+
...HUB_IDENTITY_FILES,
|
|
582
|
+
[ROUTES_PATH]: 'dd-core',
|
|
583
|
+
'./engine-private/conf/dd-core/conf.server.json': JSON.stringify({
|
|
584
|
+
'app.fixture.test': { '/': { client: 'App', proxy: [80, 443] } },
|
|
585
|
+
}),
|
|
586
|
+
...files,
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it('validates the candidate config before signalling the running daemon', () => {
|
|
591
|
+
routeFixture();
|
|
592
|
+
harness.route({ match: 'haproxy -c -f', code: 0, stdout: 'Configuration file is valid\n' });
|
|
593
|
+
const result = UnderpostWireguard.API.haproxySync({});
|
|
594
|
+
expect(result.routes.length).to.be.above(0);
|
|
595
|
+
const checkIndex = harness.calls.findIndex((command) => command.includes('haproxy -c -f'));
|
|
596
|
+
const reloadIndex = harness.calls.findIndex((command) => command.includes('reload'));
|
|
597
|
+
expect(checkIndex).to.be.above(-1);
|
|
598
|
+
expect(reloadIndex).to.be.above(checkIndex);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// A config that does not parse would take the whole edge down on reload.
|
|
602
|
+
it('restores the previous files and fails when HAProxy rejects the candidate', () => {
|
|
603
|
+
routeFixture({
|
|
604
|
+
[`${UNDERPOST_EDGE.haproxyDir}/${UNDERPOST_EDGE.haproxyConfName}`]: 'previous conf\n',
|
|
605
|
+
});
|
|
606
|
+
harness.route({ match: 'haproxy -c -f', code: 1, stdout: 'parsing error on line 3\n' });
|
|
607
|
+
expect(() => UnderpostWireguard.API.haproxySync({})).to.throw('rejected the generated config');
|
|
608
|
+
expect(harness.ran('rm -f /etc/haproxy/domain2backend.map')).to.equal(true);
|
|
609
|
+
expect(harness.ran('reload')).to.equal(false);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('publishes nothing and reloads nothing under a dry run', () => {
|
|
613
|
+
routeFixture();
|
|
614
|
+
const result = UnderpostWireguard.API.haproxySync({ dryRun: true });
|
|
615
|
+
expect(result.changed).to.equal(true);
|
|
616
|
+
expect(harness.ran('haproxy -c -f')).to.equal(false);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('refuses to publish an edge that would answer every request with a refusal', () => {
|
|
620
|
+
routeFixture({
|
|
621
|
+
[EDGE_TOPOLOGY_PATH]: JSON.stringify({ [HUB_HOST]: { ...TOPOLOGY[HUB_HOST], peers: [] } }),
|
|
622
|
+
});
|
|
623
|
+
expect(() => UnderpostWireguard.API.haproxySync({})).to.throw('No hostname resolved to a spoke');
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('refuses to publish routing from a spoke', () => {
|
|
627
|
+
edgeFixture({
|
|
628
|
+
files: {
|
|
629
|
+
...SPOKE_IDENTITY_FILES,
|
|
630
|
+
[ROUTES_PATH]: 'dd-core',
|
|
631
|
+
'./engine-private/conf/dd-core/conf.server.json': JSON.stringify({
|
|
632
|
+
'app.fixture.test': { '/': { client: 'App', proxy: [443] } },
|
|
633
|
+
}),
|
|
634
|
+
},
|
|
635
|
+
hostname: 'control-node',
|
|
636
|
+
});
|
|
637
|
+
expect(() => UnderpostWireguard.API.haproxySync({})).to.throw("requires 'haproxy'");
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('installs, publishes and enables the daemon in one pass', () => {
|
|
641
|
+
routeFixture();
|
|
642
|
+
harness.route({ match: 'command -v dnf', code: 0, stdout: '/usr/bin/dnf\n' });
|
|
643
|
+
harness.route({ match: 'rpm -q', code: 0 });
|
|
644
|
+
harness.route({ match: 'haproxy -c -f', code: 0, stdout: 'Configuration file is valid\n' });
|
|
645
|
+
UnderpostWireguard.API.haproxySetup({});
|
|
646
|
+
expect(harness.ran('systemctl enable --now haproxy')).to.equal(true);
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
describe('interface lifecycle', () => {
|
|
651
|
+
it('enables the unit and installs the QUIC forward on a hub', () => {
|
|
652
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
653
|
+
UnderpostWireguard.API.start({});
|
|
654
|
+
expect(harness.ran('systemctl enable --now wg-quick@wg0')).to.equal(true);
|
|
655
|
+
expect(harness.ran('10.0.0.2')).to.equal(true);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('enables only the unit on a spoke', () => {
|
|
659
|
+
edgeFixture({ files: SPOKE_IDENTITY_FILES, hostname: 'control-node' });
|
|
660
|
+
UnderpostWireguard.API.start({});
|
|
661
|
+
expect(harness.ran('systemctl enable --now wg-quick@wg0')).to.equal(true);
|
|
662
|
+
expect(harness.ran('DNAT')).to.equal(false);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('renegotiates a stale but active tunnel', () => {
|
|
666
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
667
|
+
UnderpostWireguard.API.restart({});
|
|
668
|
+
expect(harness.ran('systemctl restart wg-quick@wg0')).to.equal(true);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('takes the tunnel down and withdraws the rules that only apply while it is up', () => {
|
|
672
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
673
|
+
UnderpostWireguard.API.stop({});
|
|
674
|
+
expect(harness.ran('systemctl disable --now wg-quick@wg0')).to.equal(true);
|
|
675
|
+
expect(harness.ran('wg-quick down wg0')).to.equal(true);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('reports a healthy interface', () => {
|
|
679
|
+
edgeFixture({ files: SPOKE_IDENTITY_FILES, hostname: 'control-node' });
|
|
680
|
+
harness.route({ match: 'is-active', code: 0, stdout: 'active\n' });
|
|
681
|
+
harness.route({
|
|
682
|
+
match: 'latest-handshakes',
|
|
683
|
+
code: 0,
|
|
684
|
+
stdout: `hubPublicKeyFixture=\t${Math.floor(Date.now() / 1000)}\n`,
|
|
685
|
+
});
|
|
686
|
+
const report = UnderpostWireguard.API.check({ checkTimeout: 0 });
|
|
687
|
+
expect(report.ok).to.equal(true);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it('fails the run when the interface never becomes healthy', () => {
|
|
691
|
+
const previousExitCode = process.exitCode;
|
|
692
|
+
edgeFixture({ files: SPOKE_IDENTITY_FILES, hostname: 'control-node' });
|
|
693
|
+
harness.route({ match: 'is-active', code: 3, stdout: 'inactive\n' });
|
|
694
|
+
harness.route({ match: 'latest-handshakes', code: 0, stdout: '' });
|
|
695
|
+
try {
|
|
696
|
+
expect(UnderpostWireguard.API.check({ checkTimeout: 0 }).ok).to.equal(false);
|
|
697
|
+
expect(process.exitCode).to.equal(1);
|
|
698
|
+
} finally {
|
|
699
|
+
process.exitCode = previousExitCode;
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
describe('reset and reinstall', () => {
|
|
705
|
+
it('withdraws every artifact the setup installed, keeping the key and topology', () => {
|
|
706
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
707
|
+
UnderpostWireguard.API.reset({});
|
|
708
|
+
expect(harness.ran('rm -f /etc/wireguard/wg0.conf')).to.equal(true);
|
|
709
|
+
expect(harness.ran(`rm -f ${UNDERPOST_EDGE.sysctlPath}`)).to.equal(true);
|
|
710
|
+
// Deleting the maps while leaving the config that reads them behind makes
|
|
711
|
+
// the daemon fail to start on a config referencing missing files.
|
|
712
|
+
expect(harness.ran('rm -f /etc/haproxy/haproxy.cfg')).to.equal(true);
|
|
713
|
+
expect(harness.ran('rm -f /etc/haproxy/domain2backend.map')).to.equal(true);
|
|
714
|
+
expect(harness.ran('rm -f /etc/wireguard/wg0.key')).to.equal(false);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('drops the key pair and re-keys on a reinstall', () => {
|
|
718
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
719
|
+
harness.route({ match: 'sudo test -s', code: 0 });
|
|
720
|
+
harness.route({ match: 'sudo cat', code: 0, stdout: 'reKeyedHubKey=\n' });
|
|
721
|
+
UnderpostWireguard.API.reinstall({});
|
|
722
|
+
expect(harness.ran('rm -f /etc/wireguard/wg0.key')).to.equal(true);
|
|
723
|
+
expect(harness.ran('rm -f /etc/wireguard/wg0.pub')).to.equal(true);
|
|
724
|
+
expect(harness.ran('dnf -y reinstall wireguard-tools haproxy iptables')).to.equal(true);
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
describe('status', () => {
|
|
729
|
+
it('reports topology and routing without probing the host under --build-conf', () => {
|
|
730
|
+
edgeFixture({
|
|
731
|
+
files: {
|
|
732
|
+
...HUB_IDENTITY_FILES,
|
|
733
|
+
[ROUTES_PATH]: 'dd-core',
|
|
734
|
+
'./engine-private/conf/dd-core/conf.server.json': JSON.stringify({
|
|
735
|
+
'app.fixture.test': { '/': { client: 'App', proxy: [443] } },
|
|
736
|
+
}),
|
|
737
|
+
},
|
|
738
|
+
});
|
|
739
|
+
const summary = UnderpostWireguard.API.status({ buildConf: true });
|
|
740
|
+
expect(summary).to.include({ role: 'hub', nodeName: 'hub-node' });
|
|
741
|
+
expect(summary).not.to.have.property('wireguard');
|
|
742
|
+
expect(summary.routing.deployList).to.deep.equal(['dd-core']);
|
|
743
|
+
expect(harness.calls.length).to.equal(0);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it('probes both daemons and the live peers on a hub', () => {
|
|
747
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
748
|
+
harness.route({ match: 'is-active', code: 0, stdout: 'active\n' });
|
|
749
|
+
harness.route({
|
|
750
|
+
match: 'latest-handshakes',
|
|
751
|
+
code: 0,
|
|
752
|
+
stdout: `peerAPublicKeyFixture=\t${Math.floor(Date.now() / 1000)}\n`,
|
|
753
|
+
});
|
|
754
|
+
const summary = UnderpostWireguard.API.status({});
|
|
755
|
+
expect(summary.wireguard).to.equal('active');
|
|
756
|
+
expect(summary.haproxy).to.equal('active');
|
|
757
|
+
expect(summary.peers.find((peer) => peer.id === 'control-a').online).to.equal(true);
|
|
758
|
+
// Route table unavailable without a deploy conf; reported as null rather
|
|
759
|
+
// than failing the whole report.
|
|
760
|
+
expect(summary.routing).to.equal(null);
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
it('reports the hub link from a spoke', () => {
|
|
764
|
+
edgeFixture({ files: SPOKE_IDENTITY_FILES, hostname: 'control-node' });
|
|
765
|
+
harness.route({ match: 'is-active', code: 0, stdout: 'active\n' });
|
|
766
|
+
const summary = UnderpostWireguard.API.status({});
|
|
767
|
+
expect(summary.hubPublicKey).to.equal('hubPublicKeyFixture=');
|
|
768
|
+
expect(summary.hub).to.be.an('object');
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
describe('forward proxy service', () => {
|
|
773
|
+
const withApiKey = (run) => {
|
|
774
|
+
const previous = process.env[FORWARD_PROXY.env.apiKey];
|
|
775
|
+
process.env[FORWARD_PROXY.env.apiKey] = 'fixture-api-key';
|
|
776
|
+
try {
|
|
777
|
+
return run();
|
|
778
|
+
} finally {
|
|
779
|
+
if (previous === undefined) delete process.env[FORWARD_PROXY.env.apiKey];
|
|
780
|
+
else process.env[FORWARD_PROXY.env.apiKey] = previous;
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
it('binds the proxy to the hub tunnel address', () => {
|
|
785
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
786
|
+
const config = withApiKey(() => UnderpostWireguard.API.forwardProxyConfig({}));
|
|
787
|
+
expect(config.host).to.equal('10.0.0.1');
|
|
788
|
+
expect(config.tunnelCidr).to.equal('10.0.0.0/24');
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it('refuses to run on a spoke', () => {
|
|
792
|
+
edgeFixture({ files: SPOKE_IDENTITY_FILES, hostname: 'control-node' });
|
|
793
|
+
expect(() => withApiKey(() => UnderpostWireguard.API.forwardProxyConfig({}))).to.throw(
|
|
794
|
+
"requires 'forward-proxy'",
|
|
795
|
+
);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it('names the ways to configure the key when it is unset', () => {
|
|
799
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
800
|
+
const previous = process.env[FORWARD_PROXY.env.apiKey];
|
|
801
|
+
delete process.env[FORWARD_PROXY.env.apiKey];
|
|
802
|
+
try {
|
|
803
|
+
expect(() => UnderpostWireguard.API.forwardProxyConfig({})).to.throw('is not set');
|
|
804
|
+
} finally {
|
|
805
|
+
if (previous !== undefined) process.env[FORWARD_PROXY.env.apiKey] = previous;
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// Bound to a wildcard the proxy is reachable from every interface, and only
|
|
810
|
+
// the API key refuses a request.
|
|
811
|
+
it('warns about a wildcard bind rather than refusing it', () => {
|
|
812
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
813
|
+
const config = withApiKey(() => UnderpostWireguard.API.forwardProxyConfig({ forwardProxyServerHost: '0.0.0.0' }));
|
|
814
|
+
expect(config.host).to.equal('0.0.0.0');
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it('passes over a Node binary that is absent, too old, or unusable by a unit', () => {
|
|
818
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
819
|
+
harness.route({ match: 'sudo test -x', code: 1 });
|
|
820
|
+
const result = UnderpostWireguard.API.forwardProxyNodePath();
|
|
821
|
+
expect(result.path).to.equal('');
|
|
822
|
+
expect(result.rejected.every(({ reason }) => reason === 'not present')).to.equal(true);
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it('selects the first Node binary new enough for the engine', () => {
|
|
826
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
827
|
+
const required = `${process.versions.node}`.split('.')[0];
|
|
828
|
+
harness.route({ match: 'sudo test -x', code: 0 });
|
|
829
|
+
harness.route({ match: '--version', code: 0, stdout: `v${required}.0.0\n` });
|
|
830
|
+
const result = UnderpostWireguard.API.forwardProxyNodePath();
|
|
831
|
+
expect(result.path).not.to.equal('');
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it('rejects a Node binary older than the engine requires', () => {
|
|
835
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
836
|
+
harness.route({ match: 'sudo test -x', code: 0 });
|
|
837
|
+
harness.route({ match: '--version', code: 0, stdout: 'v18.0.0\n' });
|
|
838
|
+
const result = UnderpostWireguard.API.forwardProxyNodePath();
|
|
839
|
+
expect(result.path).to.equal('');
|
|
840
|
+
expect(result.rejected[0].reason).to.include('the engine needs');
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('reports the unit it would install under a dry run', () => {
|
|
844
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
845
|
+
expect(withApiKey(() => UnderpostWireguard.API.forwardProxyServer({ dryRun: true }))).to.equal(null);
|
|
846
|
+
expect(harness.ran('systemctl')).to.equal(false);
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
it('withdraws a broken unit and fails when no Node binary can run the service', () => {
|
|
850
|
+
edgeFixture({ files: { ...HUB_IDENTITY_FILES, [FORWARD_PROXY.unitPath]: 'stale unit\n' } });
|
|
851
|
+
harness.route({ match: 'sudo test -x', code: 1 });
|
|
852
|
+
expect(() => withApiKey(() => UnderpostWireguard.API.forwardProxyServer({}))).to.throw(
|
|
853
|
+
'No Node binary the forward proxy service can execute',
|
|
854
|
+
);
|
|
855
|
+
expect(harness.ran(`rm -f ${FORWARD_PROXY.unitPath}`)).to.equal(true);
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
describe('fleet sync', () => {
|
|
860
|
+
beforeEach(() => {
|
|
861
|
+
vi.spyOn(UnderpostRepository.API, 'getDefaultBranch').mockReturnValue('master');
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it('composes the sync sequence against the resolved repositories and branch', () => {
|
|
865
|
+
const previous = process.env.GITHUB_USERNAME;
|
|
866
|
+
process.env.GITHUB_USERNAME = 'fixture-org';
|
|
867
|
+
try {
|
|
868
|
+
const commands = UnderpostWireguard.API.syncCommands({});
|
|
869
|
+
expect(commands.every(({ command }) => !command.includes('<engine'))).to.equal(true);
|
|
870
|
+
expect(commands.some(({ command }) => command.includes('fixture-org/engine-private'))).to.equal(true);
|
|
871
|
+
expect(commands.some(({ command }) => command.includes('master'))).to.equal(true);
|
|
872
|
+
} finally {
|
|
873
|
+
if (previous === undefined) delete process.env.GITHUB_USERNAME;
|
|
874
|
+
else process.env.GITHUB_USERNAME = previous;
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
it.skipIf(!shipsCyberiaPackageScript)(
|
|
879
|
+
'installs the checkout manifest before the CLI is used, and the deploy manifest on what the switch landed',
|
|
880
|
+
() => {
|
|
881
|
+
// Regression: a node whose install no longer matched its manifest could not load its own
|
|
882
|
+
// CLI, so the first step of the sync died before the step that repairs the tree. The
|
|
883
|
+
// bootstrap installs the checkout's own manifest because that is the one matching the code
|
|
884
|
+
// already in the tree; the deploy manifest can only be installed once its switch lands it.
|
|
885
|
+
const previousUser = process.env.GITHUB_USERNAME;
|
|
886
|
+
const previousToken = process.env.GITHUB_TOKEN;
|
|
887
|
+
process.env.GITHUB_USERNAME = 'fixture-org';
|
|
888
|
+
process.env.GITHUB_TOKEN = 'fixture-token';
|
|
889
|
+
try {
|
|
890
|
+
const steps = UnderpostWireguard.API.syncCommands({
|
|
891
|
+
repoEngine: 'fixture-org/engine-test-cyberia',
|
|
892
|
+
nodeRole: 'control',
|
|
893
|
+
});
|
|
894
|
+
const commands = steps.map(({ command }) => command);
|
|
895
|
+
const packageStep = 'bash ./deploy/dd-cyberia/package.sh';
|
|
896
|
+
|
|
897
|
+
expect(commands[0]).to.equal('git checkout -- package.json; npm install');
|
|
898
|
+
expect(steps[0].halt, 'a tree that cannot be restored must still reach the switch').to.not.equal(true);
|
|
899
|
+
expect(commands).to.not.include('npm link --force');
|
|
900
|
+
const last = commands.lastIndexOf(packageStep);
|
|
901
|
+
expect(last).to.equal(commands.indexOf(packageStep));
|
|
902
|
+
expect(last).to.be.greaterThan(commands.findIndex((command) => command.includes('./engine-private')));
|
|
903
|
+
expect(last).to.be.lessThan(commands.findIndex((command) => command.includes('underpost-event')));
|
|
904
|
+
expect(steps[last].halt).to.equal(true);
|
|
905
|
+
|
|
906
|
+
// The monorepo belongs to no deploy, so its post-switch install falls back to the
|
|
907
|
+
// checkout's own manifest rather than being dropped.
|
|
908
|
+
const monorepo = UnderpostWireguard.API.syncCommands({
|
|
909
|
+
repoEngine: 'fixture-org/engine',
|
|
910
|
+
nodeRole: 'control',
|
|
911
|
+
}).map(({ command }) => command);
|
|
912
|
+
expect(monorepo.every((command) => !command.includes('package.sh') && !/<[a-z-]+>/.test(command))).to.equal(
|
|
913
|
+
true,
|
|
914
|
+
);
|
|
915
|
+
expect(monorepo).to.include('npm install');
|
|
916
|
+
} finally {
|
|
917
|
+
if (previousUser === undefined) delete process.env.GITHUB_USERNAME;
|
|
918
|
+
else process.env.GITHUB_USERNAME = previousUser;
|
|
919
|
+
if (previousToken === undefined) delete process.env.GITHUB_TOKEN;
|
|
920
|
+
else process.env.GITHUB_TOKEN = previousToken;
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
);
|
|
924
|
+
|
|
925
|
+
it('enters the CLI through Node, never through the link its own steps invalidate', () => {
|
|
926
|
+
// Regression: the global `underpost` is a symlink onto `bin/index.js` in the checkout the
|
|
927
|
+
// sync restores, so `run clean` rewrote that file's mode and every later step died on
|
|
928
|
+
// `Permission denied`.
|
|
929
|
+
const previous = process.env.GITHUB_USERNAME;
|
|
930
|
+
process.env.GITHUB_USERNAME = 'fixture-org';
|
|
931
|
+
try {
|
|
932
|
+
for (const nodeRole of ['control', 'worker', 'hub'])
|
|
933
|
+
expect(UnderpostWireguard.API.syncCommands({ nodeRole })).to.satisfy((steps) =>
|
|
934
|
+
steps.every(({ command }) => !/(^|[\s;&|])underpost\s/.test(command)),
|
|
935
|
+
);
|
|
936
|
+
} finally {
|
|
937
|
+
if (previous === undefined) delete process.env.GITHUB_USERNAME;
|
|
938
|
+
else process.env.GITHUB_USERNAME = previous;
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
it('never writes a blank token onto a node', () => {
|
|
943
|
+
const previous = process.env.GITHUB_TOKEN;
|
|
944
|
+
delete process.env.GITHUB_TOKEN;
|
|
945
|
+
try {
|
|
946
|
+
expect(UnderpostWireguard.API.syncCommands({})).to.satisfy((steps) =>
|
|
947
|
+
steps.every(({ command }) => !command.includes('GITHUB_TOKEN')),
|
|
948
|
+
);
|
|
949
|
+
} finally {
|
|
950
|
+
if (previous !== undefined) process.env.GITHUB_TOKEN = previous;
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
// A node is reached once, not once per step: each SSH session re-reads the
|
|
955
|
+
// credential store and re-enters the checkout.
|
|
956
|
+
it('composes the whole sequence into one remote command, halting only where it must', () => {
|
|
957
|
+
const script = UnderpostWireguard.API.syncScript({ nodeRole: 'control' });
|
|
958
|
+
expect(script.split(' && echo ').length).to.be.above(1);
|
|
959
|
+
expect(script).to.include('[sync]');
|
|
960
|
+
expect(script).to.include('|| true; }');
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
it('joins both registries into one target list, hubs first', () => {
|
|
964
|
+
vi.spyOn(UnderpostEvent.API, 'hubs').mockReturnValue([{ nodeName: 'hub-node', hubHost: HUB_HOST }]);
|
|
965
|
+
vi.spyOn(UnderpostEvent.API, 'spokes').mockReturnValue([{ nodeName: 'control-node', id: 'control-a' }]);
|
|
966
|
+
vi.spyOn(UnderpostEvent.API, 'hubTarget').mockReturnValue({ role: 'hub', via: 'local', host: HUB_HOST });
|
|
967
|
+
vi.spyOn(UnderpostEvent.API, 'spokeTarget').mockReturnValue({ role: 'spoke', via: 'ssh', host: '10.0.0.2' });
|
|
968
|
+
|
|
969
|
+
expect(UnderpostWireguard.API.syncTargets({}).map(({ nodeName }) => nodeName)).to.deep.equal([
|
|
970
|
+
'hub-node',
|
|
971
|
+
'control-node',
|
|
972
|
+
]);
|
|
973
|
+
expect(UnderpostWireguard.API.syncTargets({ nodes: 'control-a' }).map(({ nodeName }) => nodeName)).to.deep.equal([
|
|
974
|
+
'control-node',
|
|
975
|
+
]);
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
it('refuses a selector that matches no registered node', () => {
|
|
979
|
+
vi.spyOn(UnderpostEvent.API, 'hubs').mockReturnValue([]);
|
|
980
|
+
vi.spyOn(UnderpostEvent.API, 'spokes').mockReturnValue([]);
|
|
981
|
+
expect(() => UnderpostWireguard.API.syncTargets({ nodes: 'ghost' })).to.throw('no registered node matches');
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
// The URI is the connection the cluster already holds: the node document
|
|
985
|
+
// names the machine, and the address it resolves to is what the SSH
|
|
986
|
+
// registry is keyed by.
|
|
987
|
+
it('builds one connection URI per selected node, from the node name', () => {
|
|
988
|
+
vi.spyOn(UnderpostEvent.API, 'hubs').mockReturnValue([{ nodeName: 'hub-node', hubHost: HUB_HOST }]);
|
|
989
|
+
vi.spyOn(UnderpostEvent.API, 'spokes').mockReturnValue([{ nodeName: 'control-node', id: 'control-a' }]);
|
|
990
|
+
vi.spyOn(UnderpostEvent.API, 'hubTarget').mockReturnValue({
|
|
991
|
+
role: 'hub',
|
|
992
|
+
via: `root@${HUB_HOST}:22`,
|
|
993
|
+
user: 'root',
|
|
994
|
+
host: HUB_HOST,
|
|
995
|
+
port: 22,
|
|
996
|
+
keyPath: './engine-private/deploy/id_rsa',
|
|
997
|
+
});
|
|
998
|
+
vi.spyOn(UnderpostEvent.API, 'spokeTarget').mockReturnValue({
|
|
999
|
+
role: 'spoke',
|
|
1000
|
+
via: 'admin@198.51.100.2:2222',
|
|
1001
|
+
user: 'admin',
|
|
1002
|
+
host: '198.51.100.2',
|
|
1003
|
+
port: 2222,
|
|
1004
|
+
keyPath: './engine-private/deploy/users/admin/id_rsa',
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
expect(UnderpostWireguard.API.connectUri({ nodes: 'control-node' })).to.deep.equal([
|
|
1008
|
+
{
|
|
1009
|
+
nodeName: 'control-node',
|
|
1010
|
+
via: 'admin@198.51.100.2:2222',
|
|
1011
|
+
uri: 'ssh admin@198.51.100.2 -i ./engine-private/deploy/users/admin/id_rsa -p 2222',
|
|
1012
|
+
},
|
|
1013
|
+
]);
|
|
1014
|
+
expect(UnderpostWireguard.API.connectUri({}).map(({ uri }) => uri)).to.deep.equal([
|
|
1015
|
+
'ssh root@203.0.113.10 -i ./engine-private/deploy/id_rsa -p 22',
|
|
1016
|
+
'ssh admin@198.51.100.2 -i ./engine-private/deploy/users/admin/id_rsa -p 2222',
|
|
1017
|
+
]);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
it('resolves no URI for a node that is this machine', () => {
|
|
1021
|
+
vi.spyOn(UnderpostEvent.API, 'hubs').mockReturnValue([]);
|
|
1022
|
+
vi.spyOn(UnderpostEvent.API, 'spokes').mockReturnValue([{ nodeName: 'control-node', id: 'control-a' }]);
|
|
1023
|
+
vi.spyOn(UnderpostEvent.API, 'spokeTarget').mockReturnValue({ role: 'spoke', via: 'local', user: '', host: '' });
|
|
1024
|
+
|
|
1025
|
+
expect(UnderpostWireguard.API.connectUri({})).to.deep.equal([
|
|
1026
|
+
{ nodeName: 'control-node', via: 'local', uri: '' },
|
|
1027
|
+
]);
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
it('reports every node it synced, and does not stop at the first failure', async () => {
|
|
1031
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
1032
|
+
vi.spyOn(UnderpostRepository.API, 'getDefaultBranch').mockReturnValue('master');
|
|
1033
|
+
vi.spyOn(UnderpostEvent.API, 'hubs').mockReturnValue([{ nodeName: 'hub-node', hubHost: HUB_HOST }]);
|
|
1034
|
+
vi.spyOn(UnderpostEvent.API, 'spokes').mockReturnValue([{ nodeName: 'control-node', id: 'control-a' }]);
|
|
1035
|
+
// Both carry the user a real target resolves with: sync dispatches over SSH only, and
|
|
1036
|
+
// skips any node registered at an address this machine holds.
|
|
1037
|
+
vi.spyOn(UnderpostEvent.API, 'hubTarget').mockReturnValue({
|
|
1038
|
+
role: 'hub',
|
|
1039
|
+
via: 'ssh',
|
|
1040
|
+
user: 'root',
|
|
1041
|
+
host: HUB_HOST,
|
|
1042
|
+
});
|
|
1043
|
+
// A documentation address, not the fixture's 10.0.0.2: that tunnel address is a real
|
|
1044
|
+
// address on a fleet node, and the guard would skip it there instead of dispatching.
|
|
1045
|
+
vi.spyOn(UnderpostEvent.API, 'spokeTarget').mockReturnValue({
|
|
1046
|
+
role: 'spoke',
|
|
1047
|
+
via: 'ssh',
|
|
1048
|
+
user: 'admin',
|
|
1049
|
+
host: '198.51.100.2',
|
|
1050
|
+
});
|
|
1051
|
+
const run = vi
|
|
1052
|
+
.spyOn(UnderpostEvent.API, 'runCommand')
|
|
1053
|
+
.mockImplementation(async (_command, options) => ({ ok: options.host === HUB_HOST, error: 'refused' }));
|
|
1054
|
+
|
|
1055
|
+
const result = await UnderpostWireguard.API.sync({});
|
|
1056
|
+
|
|
1057
|
+
expect(run.mock.calls.length).to.equal(2);
|
|
1058
|
+
expect(result.ok).to.equal(false);
|
|
1059
|
+
expect(result.nodes.map(({ ok }) => ok)).to.deep.equal([true, false]);
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
// Cluster nodes already run the collector as a DaemonSet pod; a second copy
|
|
1063
|
+
// there would bind a port the pod holds.
|
|
1064
|
+
it('provisions the host metrics collector on hubs only', async () => {
|
|
1065
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
1066
|
+
vi.spyOn(UnderpostEvent.API, 'hubs').mockReturnValue([{ nodeName: 'hub-node', hubHost: HUB_HOST }]);
|
|
1067
|
+
vi.spyOn(UnderpostEvent.API, 'spokes').mockReturnValue([{ nodeName: 'control-node', id: 'control-a' }]);
|
|
1068
|
+
vi.spyOn(UnderpostEvent.API, 'hubTarget').mockReturnValue({ role: 'hub', via: 'local', host: HUB_HOST });
|
|
1069
|
+
vi.spyOn(UnderpostEvent.API, 'spokeTarget').mockReturnValue({ role: 'spoke', via: 'ssh', host: '10.0.0.2' });
|
|
1070
|
+
const run = vi.spyOn(UnderpostEvent.API, 'runCommand').mockResolvedValue({ ok: true });
|
|
1071
|
+
|
|
1072
|
+
const result = await UnderpostWireguard.API.nodeExporter({});
|
|
1073
|
+
|
|
1074
|
+
expect(run.mock.calls.length).to.equal(1);
|
|
1075
|
+
expect(result.ok).to.equal(true);
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
it('refuses to provision the collector when no hub matches', async () => {
|
|
1079
|
+
vi.spyOn(UnderpostEvent.API, 'hubs').mockReturnValue([]);
|
|
1080
|
+
vi.spyOn(UnderpostEvent.API, 'spokes').mockReturnValue([{ nodeName: 'control-node', id: 'control-a' }]);
|
|
1081
|
+
vi.spyOn(UnderpostEvent.API, 'spokeTarget').mockReturnValue({ role: 'spoke', via: 'ssh', host: '10.0.0.2' });
|
|
1082
|
+
await expect(UnderpostWireguard.API.nodeExporter({})).rejects.toThrow('no hub node');
|
|
1083
|
+
});
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
describe('CLI dispatch', () => {
|
|
1087
|
+
it('runs the lifecycle flags in the only order that works', async () => {
|
|
1088
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
1089
|
+
const order = [];
|
|
1090
|
+
for (const method of ['install', 'setup', 'start', 'status']) {
|
|
1091
|
+
vi.spyOn(UnderpostWireguard.API, method).mockImplementation(() => order.push(method));
|
|
1092
|
+
}
|
|
1093
|
+
await UnderpostWireguard.API.callback({
|
|
1094
|
+
status: true,
|
|
1095
|
+
wireguardStart: true,
|
|
1096
|
+
wireguardSetup: true,
|
|
1097
|
+
wireguardInstall: true,
|
|
1098
|
+
});
|
|
1099
|
+
expect(order).to.deep.equal(['install', 'setup', 'start', 'status']);
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
// `--build-conf` is a hard promise: it short-circuits every host action even
|
|
1103
|
+
// when other lifecycle flags are present.
|
|
1104
|
+
it('touches no host action under --build-conf', async () => {
|
|
1105
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
1106
|
+
const called = [];
|
|
1107
|
+
for (const method of ['install', 'start', 'stop', 'setup', 'buildConf']) {
|
|
1108
|
+
vi.spyOn(UnderpostWireguard.API, method).mockImplementation(() => called.push(method));
|
|
1109
|
+
}
|
|
1110
|
+
await UnderpostWireguard.API.callback({ buildConf: true, wireguardInstall: true, wireguardStart: true });
|
|
1111
|
+
expect(called).to.deep.equal(['buildConf']);
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
it('normalizes topology when --build-conf carries no other action', async () => {
|
|
1115
|
+
edgeFixture({ files: HUB_IDENTITY_FILES });
|
|
1116
|
+
const buildConf = vi.spyOn(UnderpostWireguard.API, 'buildConf').mockImplementation(() => ({}));
|
|
1117
|
+
await UnderpostWireguard.API.callback({ buildConf: true });
|
|
1118
|
+
expect(buildConf.mock.calls.length).to.equal(1);
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
it('records an identity and stops when no host action was requested', async () => {
|
|
1122
|
+
const nodeConfig = vi.spyOn(UnderpostWireguard.API, 'nodeConfig').mockImplementation(() => ({}));
|
|
1123
|
+
const setup = vi.spyOn(UnderpostWireguard.API, 'setup').mockImplementation(() => ({}));
|
|
1124
|
+
await UnderpostWireguard.API.callback({ nodeConfig: true });
|
|
1125
|
+
expect(nodeConfig.mock.calls.length).to.equal(1);
|
|
1126
|
+
expect(setup.mock.calls.length).to.equal(0);
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
it('takes reinstall and reset before every other host action', async () => {
|
|
1130
|
+
const reinstall = vi.spyOn(UnderpostWireguard.API, 'reinstall').mockImplementation(() => undefined);
|
|
1131
|
+
const reset = vi.spyOn(UnderpostWireguard.API, 'reset').mockImplementation(() => undefined);
|
|
1132
|
+
const install = vi.spyOn(UnderpostWireguard.API, 'install').mockImplementation(() => undefined);
|
|
1133
|
+
|
|
1134
|
+
await UnderpostWireguard.API.callback({ wireguardReinstall: true, wireguardInstall: true });
|
|
1135
|
+
expect(reinstall.mock.calls.length).to.equal(1);
|
|
1136
|
+
expect(install.mock.calls.length).to.equal(0);
|
|
1137
|
+
|
|
1138
|
+
await UnderpostWireguard.API.callback({ wireguardReset: true, wireguardInstall: true });
|
|
1139
|
+
expect(reset.mock.calls.length).to.equal(1);
|
|
1140
|
+
expect(install.mock.calls.length).to.equal(0);
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
it('prefers the full HAProxy setup over a bare sync', async () => {
|
|
1144
|
+
const setup = vi.spyOn(UnderpostWireguard.API, 'haproxySetup').mockImplementation(() => undefined);
|
|
1145
|
+
const sync = vi.spyOn(UnderpostWireguard.API, 'haproxySync').mockImplementation(() => undefined);
|
|
1146
|
+
await UnderpostWireguard.API.callback({ haproxySetup: true, haproxySync: true });
|
|
1147
|
+
expect(setup.mock.calls.length).to.equal(1);
|
|
1148
|
+
expect(sync.mock.calls.length).to.equal(0);
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
it('routes the fleet-wide flags away from every host action', async () => {
|
|
1152
|
+
const sync = vi.spyOn(UnderpostWireguard.API, 'sync').mockResolvedValue({ ok: true, nodes: [] });
|
|
1153
|
+
const nodeExporter = vi.spyOn(UnderpostWireguard.API, 'nodeExporter').mockResolvedValue({ ok: true, nodes: [] });
|
|
1154
|
+
const install = vi.spyOn(UnderpostWireguard.API, 'install').mockImplementation(() => undefined);
|
|
1155
|
+
|
|
1156
|
+
await UnderpostWireguard.API.callback({ sync: true, wireguardInstall: true });
|
|
1157
|
+
await UnderpostWireguard.API.callback({ nodeExporter: true, wireguardInstall: true });
|
|
1158
|
+
|
|
1159
|
+
expect(sync.mock.calls.length).to.equal(1);
|
|
1160
|
+
expect(nodeExporter.mock.calls.length).to.equal(1);
|
|
1161
|
+
expect(install.mock.calls.length).to.equal(0);
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
it('routes --cmd alone as a fleet run, away from every host action', async () => {
|
|
1165
|
+
const sync = vi.spyOn(UnderpostWireguard.API, 'sync').mockResolvedValue({ ok: true, nodes: [] });
|
|
1166
|
+
const install = vi.spyOn(UnderpostWireguard.API, 'install').mockImplementation(() => undefined);
|
|
1167
|
+
|
|
1168
|
+
await UnderpostWireguard.API.callback({ cmd: 'uptime', wireguardInstall: true });
|
|
1169
|
+
|
|
1170
|
+
expect(sync.mock.calls.length).to.equal(1);
|
|
1171
|
+
expect(install.mock.calls.length).to.equal(0);
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
it('runs a single fleet dispatch when --cmd replaces the sync sequence', async () => {
|
|
1175
|
+
const sync = vi.spyOn(UnderpostWireguard.API, 'sync').mockResolvedValue({ ok: true, nodes: [] });
|
|
1176
|
+
|
|
1177
|
+
await UnderpostWireguard.API.callback({ sync: true, cmd: 'uptime, hostname' });
|
|
1178
|
+
|
|
1179
|
+
expect(sync.mock.calls.length).to.equal(1);
|
|
1180
|
+
expect(sync.mock.calls[0][0]).to.include({ cmd: 'uptime, hostname' });
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
it('exposes the same API through the CLI namespace', () => {
|
|
1184
|
+
expect(Underpost.wireguard).to.equal(UnderpostWireguard.API);
|
|
1185
|
+
});
|
|
1186
|
+
});
|
|
1187
|
+
});
|