@underpostnet/cyberia 3.2.90 → 3.3.73
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 +16 -0
- package/.github/workflows/coverall.cyberia.ci.yml +107 -0
- package/.github/workflows/cyberia-client.cd.yml +6 -18
- package/.github/workflows/cyberia-server.cd.yml +6 -18
- 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 +6 -78
- package/.github/workflows/engine-cyberia.ci.yml +4 -5
- package/.github/workflows/ghpkg.ci.yml +42 -21
- package/.github/workflows/hardhat.ci.yml +12 -11
- package/.github/workflows/npmpkg.ci.yml +17 -15
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
- package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
- package/.github/workflows/release.cd.yml +2 -9
- package/.prettierignore +1 -0
- package/AGENTS.md +47 -0
- package/CHANGELOG.md +581 -2417
- package/CLI-HELP.md +416 -127
- package/Dockerfile +25 -9
- package/Dockerfile.dev +25 -9
- package/Dockerfile.test +4 -4
- package/README.md +21 -1
- package/bin/build.js +70 -43
- package/bin/build.template.js +8 -3
- package/bin/cyberia.js +372 -135
- package/bin/deploy.js +18 -104
- package/bin/index.js +372 -135
- package/compose.env +16 -0
- package/conf.js +93 -4
- package/deploy/cyberia-client/deploy.sh +47 -0
- package/deploy/cyberia-client/package.sh +18 -0
- package/deploy/cyberia-client/state.sh +14 -0
- package/deploy/cyberia-server/deploy.sh +47 -0
- package/deploy/cyberia-server/package.sh +18 -0
- package/deploy/cyberia-server/state.sh +14 -0
- package/deploy/dd-cyberia/deploy.sh +22 -0
- package/deploy/dd-cyberia/init.sh +55 -0
- package/deploy/dd-cyberia/package.sh +17 -0
- package/deploy/dd-cyberia/state.sh +14 -0
- package/deploy/dd-cyberia/sync-deploy.sh +116 -0
- package/deploy/lib/github-actions-logging.sh +594 -0
- package/deploy/lib/host.sh +185 -0
- package/deploy/lib/state.sh +62 -0
- package/deploy/pwa-microservices-template/deploy.sh +45 -0
- package/deploy/release/deploy.sh +32 -0
- package/deployment.yaml +7 -2
- package/docker-compose.yml +31 -37
- 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 +104 -43
- package/hardhat/package.json +3 -3
- package/manifests/alertmanager/deployment.yaml +81 -0
- package/manifests/blackbox-exporter/deployment.yaml +75 -0
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +36 -7
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +77 -0
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- 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 +83 -10
- package/scripts/audit-selinux.sh +64 -0
- package/scripts/event-monitor.sh +56 -0
- package/scripts/gpu-diag.sh +0 -0
- package/scripts/ip-info.sh +0 -0
- package/scripts/k3s-node-setup.sh +30 -14
- package/scripts/kubeadm-node-setup.sh +40 -31
- package/scripts/lxd-vm-setup.sh +0 -0
- package/scripts/maas-nat-firewalld.sh +0 -0
- package/scripts/maas-setup.sh +8 -8
- package/scripts/nat-iptables.sh +2 -0
- package/scripts/rhel-grpc-setup.sh +0 -0
- package/scripts/rocky-kickstart.sh +25 -9
- package/scripts/selinux-normalize.sh +907 -0
- package/scripts/shutdown-machine.sh +843 -0
- package/scripts/test-monitor.sh +3 -3
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +1 -1
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +2 -2
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +2 -2
- 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 +1 -1
- package/src/api/cyberia-action/cyberia-action.service.js +2 -2
- package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -1
- package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +1 -1
- package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +2 -2
- package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
- 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.router.js +1 -1
- package/src/api/cyberia-entity/cyberia-entity.service.js +2 -2
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +1 -1
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +1 -1
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +2 -2
- package/src/api/cyberia-instance/cyberia-fallback-capture.js +466 -0
- package/src/api/cyberia-instance/cyberia-fallback-default-items.js +63 -0
- package/src/api/cyberia-instance/cyberia-fallback-world.js +16 -1
- package/src/api/cyberia-instance/cyberia-instance-map.service.js +1 -1
- package/src/api/cyberia-instance/cyberia-instance.controller.js +4 -1
- package/src/api/cyberia-instance/cyberia-instance.router.js +15 -2
- package/src/api/cyberia-instance/cyberia-instance.service.js +45 -2
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +1 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +2 -2
- 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 +2 -2
- package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
- package/src/api/cyberia-quest/cyberia-quest.router.js +1 -1
- package/src/api/cyberia-quest/cyberia-quest.service.js +2 -2
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.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 +16 -29
- package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
- 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.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 +204 -3
- package/src/api/object-layer/object-layer.router.js +2 -2
- package/src/api/object-layer/object-layer.service.js +4 -17
- 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 +391 -0
- package/src/cli/baremetal.js +30 -36
- package/src/cli/client.js +306 -0
- package/src/cli/cloud-init.js +3 -3
- package/src/cli/cluster.js +242 -135
- package/src/cli/db.js +126 -67
- package/src/cli/deploy.js +158 -60
- 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.js +3 -3
- package/src/cli/host.js +672 -0
- package/src/cli/image.js +4 -7
- package/src/cli/index.js +426 -134
- 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 +73 -34
- package/src/cli/repository.js +564 -367
- package/src/cli/run.js +362 -654
- package/src/cli/secrets.js +2158 -964
- package/src/cli/ssh.js +651 -331
- package/src/cli/state.js +394 -0
- package/src/cli/static.js +2 -2
- package/src/cli/system.js +26 -13
- package/src/cli/test.js +199 -125
- package/src/cli/vultr.js +623 -0
- package/src/cli/wireguard.js +2930 -0
- package/src/client/components/core/CalendarCore.js +53 -96
- package/src/client/components/core/Docs.js +258 -211
- package/src/client/components/core/FileExplorer.js +0 -21
- 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/FallbackWorldEngineCyberia.js +368 -0
- package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +35 -1
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +4 -0
- package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
- 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 +17 -3
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +32 -5
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +1 -1
- package/src/client/public/cyberia-docs/CYBERIA.md +21 -1
- 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/WHITE-PAPER.md +1 -1
- 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-instance/cyberia-instance.service.js +40 -0
- package/src/client/services/object-layer/object-layer.management.js +4 -4
- 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 +61 -46
- package/src/client-builder/client-build-live.js +2 -2
- package/src/client-builder/client-build.js +58 -44
- 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/MongooseDB.js +11 -8
- package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
- package/src/grpc/cyberia/grpc-server.js +1 -1
- package/src/index.js +103 -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 +1 -1
- package/src/projects/cyberia/besu-genesis-generator.js +3 -3
- package/src/projects/cyberia/catalog-cyberia.js +54 -6
- package/src/projects/cyberia/gemini-client.js +1 -1
- package/src/projects/cyberia/generate-saga.js +15 -24
- package/src/projects/cyberia/hot-reload-trigger.js +1 -1
- package/src/projects/cyberia/instance-data.js +22 -3
- package/src/projects/cyberia/ipfs-client.js +4 -4
- package/src/projects/cyberia/map-preview-generator.js +1 -1
- package/src/projects/cyberia/object-layer.js +12 -22
- package/src/projects/cyberia/semantic-layer-generator.js +1 -1
- package/src/projects/underpost/catalog-underpost.js +13 -2
- package/src/proxy.js +4 -7
- package/src/runtime/cyberia-client/Dockerfile +9 -4
- package/src/runtime/cyberia-client/Dockerfile.dev +8 -2
- package/src/runtime/cyberia-server/Dockerfile +9 -4
- package/src/runtime/cyberia-server/Dockerfile.dev +8 -2
- package/src/runtime/engine-cyberia/Dockerfile +25 -9
- package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
- package/src/runtime/engine-cyberia/Dockerfile.test +4 -4
- package/src/runtime/engine-cyberia/compose.env +16 -0
- package/src/runtime/engine-cyberia/docker-compose.yml +31 -37
- 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 +117 -0
- package/src/server/build/execution.js +402 -0
- package/src/server/build/package.js +465 -0
- package/src/server/build/testing.js +528 -0
- package/src/server/{dns.js → network/dns.js} +241 -14
- package/src/server/network/forward-proxy.js +541 -0
- package/src/server/{middlewares.js → network/middlewares.js} +59 -4
- 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/network/underpost-compression.js +186 -0
- package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +26 -16
- package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +19 -3
- package/src/server/{cri.js → ops/cri.js} +2 -2
- package/src/server/ops/cron.js +949 -0
- package/src/server/ops/event-notification.js +284 -0
- package/src/server/{logger.js → ops/logger.js} +108 -23
- package/src/server/ops/monitoring.js +1724 -0
- package/src/server/ops/systemd.js +259 -0
- package/src/server/{conf.js → runtime/conf.js} +521 -520
- 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 -13
- 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/security/selinux.js +185 -0
- package/src/server/{backup.js → storage/backup.js} +17 -8
- package/src/server/{data-query.js → storage/data-query.js} +1 -1
- package/src/server/{downloader.js → storage/downloader.js} +2 -2
- package/src/server/storage/repository.js +68 -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/cyberia-cli-plain-reads.test.js +30 -0
- package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +2 -1
- package/test/integration/app/cyberia/cyberia-load.test.js +467 -0
- package/test/integration/app/cyberia/fallback-world-capture.test.js +253 -0
- package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -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/integration/infra/1-security/selinux.test.js +69 -0
- 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 +1163 -0
- package/test/integration/infra/2-network/wireguard-edge.test.js +1731 -0
- 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/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} +47 -6
- package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +56 -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 +334 -0
- package/test/unit/conf-loading.test.js +303 -0
- package/test/unit/conf-resolution.test.js +979 -0
- package/test/unit/coverage-artifact.test.js +130 -0
- package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
- package/test/unit/deploy-env-resolution.test.js +38 -0
- package/test/unit/deploy-log-table.test.js +447 -0
- package/test/unit/execution-profiles.test.js +219 -0
- package/test/unit/fleet-sync-source.test.js +149 -0
- package/test/unit/logger-redaction.test.js +112 -0
- package/test/unit/package.test.js +403 -0
- package/test/unit/prepare-host.test.js +299 -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 +193 -0
- package/vitest.config.js +58 -0
- package/.nycrc +0 -9
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
- package/manifests/mongodb/pv-pvc.yaml +0 -59
- package/scripts/link-local-underpost-cli.sh +0 -6
- 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/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/cron.js +0 -476
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module config-scope.test
|
|
5
|
+
* @description Proves that a credential cannot cross the scope that owns it.
|
|
6
|
+
*
|
|
7
|
+
* The assertions here are mostly about absence, which is the half that matters: a projection that
|
|
8
|
+
* carries too much still passes every test written about what it carries. Each case names a real
|
|
9
|
+
* credential family and asserts the cron workloads cannot see it.
|
|
10
|
+
*
|
|
11
|
+
* Uses 'chai' for assertions.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { expect } from 'chai';
|
|
15
|
+
import fs from 'fs-extra';
|
|
16
|
+
import UnderpostCron, { cronJobYamlFactory, engineMirrorContentsFactory } from '../../../../src/server/ops/cron.js';
|
|
17
|
+
import UnderpostSecret from '../../../../src/cli/secrets.js';
|
|
18
|
+
import UnderpostHost from '../../../../src/cli/host.js';
|
|
19
|
+
import {
|
|
20
|
+
CONFIG_OWNERSHIP,
|
|
21
|
+
CONFIG_SCOPES,
|
|
22
|
+
SCOPE_ENTITLEMENTS,
|
|
23
|
+
classifyConfigKeys,
|
|
24
|
+
configOwnershipFactory,
|
|
25
|
+
configRejectionFactory,
|
|
26
|
+
scopeReceivesKey,
|
|
27
|
+
scopeValuesFactory,
|
|
28
|
+
} from '../../../../src/server/runtime/config-scope.js';
|
|
29
|
+
|
|
30
|
+
// One representative credential per foreign scope, with the value a leak would expose.
|
|
31
|
+
const FOREIGN_CREDENTIALS = {
|
|
32
|
+
DB_PASSWORD: 'app-database-secret',
|
|
33
|
+
JWT_SECRET: 'app-session-secret',
|
|
34
|
+
CLOUDINARY_API_SECRET: 'app-integration-secret',
|
|
35
|
+
MARIADB_PASSWORD: 'app-mariadb-secret',
|
|
36
|
+
WP_ADMIN_PASSWORD_SITE: 'app-wordpress-secret',
|
|
37
|
+
MAAS_API_KEY: 'baremetal-maas-secret',
|
|
38
|
+
DB_PG_MAAS_PASS: 'baremetal-postgres-secret',
|
|
39
|
+
NPM_TOKEN: 'publishing-npm-secret',
|
|
40
|
+
DOCKER_HUB_ACCESS_TOKEN: 'publishing-registry-secret',
|
|
41
|
+
POSTMAN_PASSWORD: 'publishing-postman-secret',
|
|
42
|
+
GF_SECURITY_ADMIN_PASSWORD: 'host-grafana-secret',
|
|
43
|
+
CLUSTER_MAILER_SMTP_AUTH_PASS: 'host-mailer-secret',
|
|
44
|
+
UNDERPOST_EVENT_TOKEN: 'host-dispatcher-secret',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const CRON_CREDENTIALS = {
|
|
48
|
+
VULTR_API_KEY: 'cron-vultr',
|
|
49
|
+
DDNS_API_KEY: 'cron-ddns',
|
|
50
|
+
GITHUB_TOKEN: 'cron-github',
|
|
51
|
+
DEFAULT_SSH_HOST: '10.0.0.1',
|
|
52
|
+
FORWARD_PROXY_API_KEY: 'cron-proxy',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Entitled to no scope: consumed by nothing in a pod, and a projected key path would shadow the
|
|
56
|
+
// Secret volume that holds the actual credential.
|
|
57
|
+
const UNCONSUMED_BY_CRON = {
|
|
58
|
+
DEFAULT_SSH_KEY_PATH: '/home/dd/engine/engine-private/deploy/id_rsa',
|
|
59
|
+
VULTR_SSH_KEY_PATH: '/home/dd/engine/engine-private/deploy/id_rsa',
|
|
60
|
+
GITHUB_BACKUP_REPO: 'owner/backups',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
describe('configuration scope ownership', () => {
|
|
64
|
+
it('places every key of the live host source, or names the ones it cannot', () => {
|
|
65
|
+
// Fail-closed is the whole contract: an unplaced key is reported for a decision rather than
|
|
66
|
+
// defaulted into a scope, because both wrong answers are silent.
|
|
67
|
+
const source = './engine-private/conf/dd-cron/.env.production';
|
|
68
|
+
if (!fs.existsSync(source)) return;
|
|
69
|
+
const values = Object.fromEntries(
|
|
70
|
+
fs
|
|
71
|
+
.readFileSync(source, 'utf8')
|
|
72
|
+
.split('\n')
|
|
73
|
+
.map((line) => line.match(/^([A-Za-z_][A-Za-z0-9_]*)=/)?.[1])
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.map((key) => [key, '']),
|
|
76
|
+
);
|
|
77
|
+
expect(classifyConfigKeys(values).unclassified).to.deep.equal([]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('gives every key exactly one owner', () => {
|
|
81
|
+
for (const key of [...Object.keys(FOREIGN_CREDENTIALS), ...Object.keys(CRON_CREDENTIALS)]) {
|
|
82
|
+
const ownership = configOwnershipFactory(key);
|
|
83
|
+
expect(ownership, key).to.not.equal(null);
|
|
84
|
+
expect(Object.keys(CONFIG_SCOPES), key).to.include(ownership.owner);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('reports an unknown key rather than placing it', () => {
|
|
89
|
+
expect(configOwnershipFactory('SOME_NEW_CREDENTIAL')).to.equal(null);
|
|
90
|
+
expect(classifyConfigKeys({ SOME_NEW_CREDENTIAL: 'x' }).unclassified).to.deep.equal(['SOME_NEW_CREDENTIAL']);
|
|
91
|
+
expect(scopeReceivesKey('SOME_NEW_CREDENTIAL', 'cron')).to.equal(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('projects only by an explicit rule, never by inheritance', () => {
|
|
95
|
+
// A projection is an edit to the ownership table, so it is reviewable.
|
|
96
|
+
expect(configOwnershipFactory('GITHUB_TOKEN')).to.deep.equal({ owner: 'host', projectedTo: ['cron'] });
|
|
97
|
+
expect(configOwnershipFactory('GF_SECURITY_ADMIN_PASSWORD')).to.deep.equal({ owner: 'host', projectedTo: [] });
|
|
98
|
+
// Both are host-owned; only one reaches cron.
|
|
99
|
+
expect(scopeReceivesKey('GITHUB_TOKEN', 'cron')).to.equal(true);
|
|
100
|
+
expect(scopeReceivesKey('GF_SECURITY_ADMIN_PASSWORD', 'cron')).to.equal(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('names a rejection without disclosing the value behind it', () => {
|
|
104
|
+
const line = configRejectionFactory({
|
|
105
|
+
domain: 'cron',
|
|
106
|
+
deployId: 'dd-cron',
|
|
107
|
+
env: 'production',
|
|
108
|
+
key: 'MAAS_API_KEY',
|
|
109
|
+
reason: 'key belongs to baremetal scope',
|
|
110
|
+
});
|
|
111
|
+
expect(line).to.equal(
|
|
112
|
+
'configuration source rejected: domain=cron deploy=dd-cron env=production key=MAAS_API_KEY ' +
|
|
113
|
+
'reason=key belongs to baremetal scope',
|
|
114
|
+
);
|
|
115
|
+
expect(line).to.not.include('baremetal-maas-secret');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('orders the ownership table so a narrower rule is never shadowed', () => {
|
|
119
|
+
for (const [index, rule] of CONFIG_OWNERSHIP.entries()) {
|
|
120
|
+
const shadowed = CONFIG_OWNERSHIP.slice(0, index).find((earlier) => earlier.match.source === rule.match.source);
|
|
121
|
+
expect(shadowed, rule.match.source).to.equal(undefined);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('cron cannot see what it does not own', () => {
|
|
127
|
+
const environment = { ...FOREIGN_CREDENTIALS, ...UNCONSUMED_BY_CRON, ...CRON_CREDENTIALS };
|
|
128
|
+
|
|
129
|
+
it('entitles cron to its own scope and to nothing else', () => {
|
|
130
|
+
const entitled = scopeValuesFactory(environment, 'cron');
|
|
131
|
+
for (const key of Object.keys(CRON_CREDENTIALS)) expect(entitled, key).to.have.property(key);
|
|
132
|
+
for (const key of Object.keys(FOREIGN_CREDENTIALS)) expect(entitled, key).to.not.have.property(key);
|
|
133
|
+
for (const key of Object.keys(UNCONSUMED_BY_CRON)) expect(entitled, key).to.not.have.property(key);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('keeps every foreign credential out of the projected Secret, by value as well as by key', () => {
|
|
137
|
+
const previous = { ...process.env };
|
|
138
|
+
try {
|
|
139
|
+
Object.assign(process.env, environment);
|
|
140
|
+
const projected = UnderpostSecret.API.seedEnvKeys('underpost-cron-env');
|
|
141
|
+
const rendered = JSON.stringify(projected);
|
|
142
|
+
for (const [key, value] of Object.entries({ ...FOREIGN_CREDENTIALS, ...UNCONSUMED_BY_CRON })) {
|
|
143
|
+
expect(projected, key).to.not.have.property(key);
|
|
144
|
+
expect(rendered, value).to.not.include(value);
|
|
145
|
+
}
|
|
146
|
+
for (const key of Object.keys(CRON_CREDENTIALS)) expect(projected, key).to.have.property(key);
|
|
147
|
+
} finally {
|
|
148
|
+
for (const key of Object.keys(environment)) {
|
|
149
|
+
if (previous[key] === undefined) delete process.env[key];
|
|
150
|
+
else process.env[key] = previous[key];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('mirrors no deployment environment file into the shared tree', () => {
|
|
156
|
+
// The mirror carries the shared container label, so a single `.env.*` rule there hands every
|
|
157
|
+
// deployment's credentials to every container on the node.
|
|
158
|
+
const rules = engineMirrorContentsFactory();
|
|
159
|
+
expect(rules.at(-1)).to.equal('--exclude=*');
|
|
160
|
+
expect(rules.some((rule) => rule.includes('.env'))).to.equal(false);
|
|
161
|
+
for (const environmentName of ['production', 'development', 'test'])
|
|
162
|
+
expect(rules.join(' '), environmentName).to.not.include(`.env.${environmentName}`);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('mirrors the configuration documents, which hold references rather than values', () => {
|
|
166
|
+
const rules = engineMirrorContentsFactory();
|
|
167
|
+
expect(rules).to.include('--include=/engine-private/conf/*/conf.*.json');
|
|
168
|
+
expect(rules).to.include('--include=/engine-private/deploy/dd.cron');
|
|
169
|
+
expect(rules).to.include('--include=/engine-private/deploy/dd.routes');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('takes no environment from a file at runtime, so a mirrored one cannot be reintroduced', () => {
|
|
173
|
+
const manifest = cronJobYamlFactory({
|
|
174
|
+
name: 'dd-cron-vultr',
|
|
175
|
+
expression: '*/30 * * * *',
|
|
176
|
+
deployList: 'dd-cron',
|
|
177
|
+
jobList: 'vultr',
|
|
178
|
+
kubeadm: true,
|
|
179
|
+
});
|
|
180
|
+
expect(manifest).to.not.include('app load');
|
|
181
|
+
expect(manifest).to.include('node bin cron dd-cron vultr');
|
|
182
|
+
expect(manifest).to.include('secretRef:');
|
|
183
|
+
expect(manifest).to.include('name: underpost-cron-env');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('renders the resolved environment into the manifest, where it wins over the projection', () => {
|
|
187
|
+
const render = (dev) =>
|
|
188
|
+
cronJobYamlFactory({ name: 'x', expression: '* * * * *', deployList: 'dd-cron', jobList: 'vultr', dev });
|
|
189
|
+
expect(render(false)).to.include('- name: NODE_ENV\n value: production');
|
|
190
|
+
expect(render(true)).to.include('- name: NODE_ENV\n value: development');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('exposes the job handlers it is meant to, unchanged by the scoping', () => {
|
|
194
|
+
expect(Object.keys(UnderpostCron.JOB).sort()).to.deep.equal(['backup', 'dns', 'vultr']);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Proves the finalized policy: ownership may be a family pattern, but what a workload may *read*
|
|
199
|
+
// is an exact list. Without this, the next credential added to an owned family arrives in a pod
|
|
200
|
+
// unreviewed, which is the failure mode the entitlement table exists to prevent.
|
|
201
|
+
describe('scope entitlement is explicit, not inferred from naming', () => {
|
|
202
|
+
it('refuses a new member of an owned family, however it is named', () => {
|
|
203
|
+
for (const key of ['GITHUB_NEW_SENSITIVE_TOKEN', 'VULTR_NEW_SECRET', 'DDNS_NEW_SECRET']) {
|
|
204
|
+
expect(configOwnershipFactory(key), key).to.not.equal(null);
|
|
205
|
+
expect(scopeReceivesKey(key, 'cron'), key).to.equal(false);
|
|
206
|
+
}
|
|
207
|
+
// The named members it replaces stay entitled, so the narrowing is a policy and not an outage.
|
|
208
|
+
for (const key of ['GITHUB_TOKEN', 'VULTR_API_KEY', 'DDNS_API_KEY'])
|
|
209
|
+
expect(scopeReceivesKey(key, 'cron'), key).to.equal(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('projects nothing a workload does not consume', () => {
|
|
213
|
+
// Regression: projecting a key path made the SSH resolver prefer a host path that does not
|
|
214
|
+
// exist in a pod over the Secret volume that holds the actual key.
|
|
215
|
+
for (const key of ['DEFAULT_SSH_KEY_PATH', 'VULTR_SSH_KEY_PATH', 'GITHUB_BACKUP_REPO', 'GITHUB_DNS_REPO'])
|
|
216
|
+
expect(scopeReceivesKey(key, 'cron'), key).to.equal(false);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('names every entitled key in the table, so the policy reads without inference', () => {
|
|
220
|
+
for (const key of SCOPE_ENTITLEMENTS.cron) expect(configOwnershipFactory(key), key).to.not.equal(null);
|
|
221
|
+
expect(
|
|
222
|
+
Object.keys(scopeValuesFactory(Object.fromEntries(SCOPE_ENTITLEMENTS.cron.map((k) => [k, 'v'])), 'cron')),
|
|
223
|
+
).to.have.lengthOf(SCOPE_ENTITLEMENTS.cron.length);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('the host domain has one durable source per scope', () => {
|
|
228
|
+
// A migrated node has no `engine-private` checkout to assert against in every environment this
|
|
229
|
+
// suite runs in (CI, a fresh clone), so the migrated state is a fixture rather than the real
|
|
230
|
+
// filesystem — the deterministic path the project's testing guidance asks for, and immune to
|
|
231
|
+
// whatever an operator has actually done to this box's private tree.
|
|
232
|
+
const SCOPES_DIR = './engine-private/deploy/scopes';
|
|
233
|
+
const LEGACY_SOURCE = './engine-private/conf/dd-cron/.env.production';
|
|
234
|
+
|
|
235
|
+
const migratedNodeFixture = () => {
|
|
236
|
+
const table = new Map([
|
|
237
|
+
[`${SCOPES_DIR}/host.env.production`, 'GITHUB_TOKEN=host-github\n'],
|
|
238
|
+
[`${SCOPES_DIR}/cron.env.production`, 'DDNS_API_KEY=cron-ddns\n'],
|
|
239
|
+
[`${SCOPES_DIR}/app.env.production`, 'DB_PASSWORD=app-secret\n'],
|
|
240
|
+
]);
|
|
241
|
+
const modes = new Map([...table.keys()].map((path) => [path, 0o600]));
|
|
242
|
+
vi.spyOn(fs, 'existsSync').mockImplementation((filePath) => table.has(`${filePath}`));
|
|
243
|
+
vi.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
|
|
244
|
+
const key = `${filePath}`;
|
|
245
|
+
if (!table.has(key)) throw Object.assign(new Error(`ENOENT: ${key}`), { code: 'ENOENT' });
|
|
246
|
+
return table.get(key);
|
|
247
|
+
});
|
|
248
|
+
vi.spyOn(fs, 'writeFileSync').mockImplementation((filePath, value) => {
|
|
249
|
+
const key = `${filePath}`;
|
|
250
|
+
table.set(key, `${value}`);
|
|
251
|
+
if (!modes.has(key)) modes.set(key, 0o644);
|
|
252
|
+
});
|
|
253
|
+
vi.spyOn(fs, 'mkdirpSync').mockImplementation(() => undefined);
|
|
254
|
+
vi.spyOn(fs, 'chmodSync').mockImplementation((filePath, mode) => modes.set(`${filePath}`, mode));
|
|
255
|
+
vi.spyOn(fs, 'statSync').mockImplementation((filePath) => ({ mode: modes.get(`${filePath}`) ?? 0o644 }));
|
|
256
|
+
vi.spyOn(fs, 'removeSync').mockImplementation((filePath) => table.delete(`${filePath}`));
|
|
257
|
+
return { table, modes };
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
beforeEach(() => migratedNodeFixture());
|
|
261
|
+
afterEach(() => vi.restoreAllMocks());
|
|
262
|
+
|
|
263
|
+
it('reads the scoped sources and needs no unsplit file', () => {
|
|
264
|
+
// The migration has run in this tree: the scoped sources answer for the whole environment and
|
|
265
|
+
// the file they were split from is gone.
|
|
266
|
+
expect(UnderpostHost.API.hasDualSource('production')).to.equal(false);
|
|
267
|
+
expect(fs.existsSync(UnderpostHost.API.envPath('production'))).to.equal(false);
|
|
268
|
+
expect(UnderpostHost.API.scopedSources('production').length).to.be.greaterThan(0);
|
|
269
|
+
expect(Object.keys(UnderpostHost.API.read('production')).length).to.be.greaterThan(0);
|
|
270
|
+
expect(UnderpostHost.API.sourceLabel('production')).to.include('/scopes/');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('keeps every scoped source unreadable to anyone but its owner', () => {
|
|
274
|
+
for (const { path } of UnderpostHost.API.scopedSources('production'))
|
|
275
|
+
expect(fs.statSync(path).mode & 0o777, path).to.equal(0o600);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('retires nothing while an unsplit source is unaccounted for, and is idempotent once it is', () => {
|
|
279
|
+
expect(UnderpostHost.API.retireLegacySource({ env: 'production' })).to.include({ reason: 'already retired' });
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Regression: after retirement these read a file that no longer exists, so `host setup` — which
|
|
283
|
+
// composes them — threw on every migrated node. The finished state of a migration is not a
|
|
284
|
+
// broken configuration. Asserted on the two operations rather than through `setup`, whose
|
|
285
|
+
// `load` rewrites the host store once per key and belongs to no timing budget here.
|
|
286
|
+
it('reruns on a migrated node with the legacy source gone', () => {
|
|
287
|
+
expect(fs.existsSync(LEGACY_SOURCE)).to.equal(false);
|
|
288
|
+
expect(() => UnderpostHost.API.split({ env: 'production' })).to.not.throw();
|
|
289
|
+
expect(UnderpostHost.API.split({ env: 'production' }).written).to.deep.equal([]);
|
|
290
|
+
expect(UnderpostHost.API.verifySplit({ env: 'production' }).ok).to.equal(true);
|
|
291
|
+
expect(UnderpostHost.API.hasDualSource('production')).to.equal(false);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('keeps 0600 across a re-split', () => {
|
|
295
|
+
UnderpostHost.API.split({ env: 'production' });
|
|
296
|
+
for (const { path } of UnderpostHost.API.scopedSources('production'))
|
|
297
|
+
expect(fs.statSync(path).mode & 0o777, path).to.equal(0o600);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('refuses to place a key no scope claims rather than parking it somewhere', () => {
|
|
301
|
+
expect(() => UnderpostHost.API.writeScopes({ TOTALLY_NEW_CREDENTIAL: 'x' }, { dryRun: true })).to.throw(
|
|
302
|
+
'belong to no scope',
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { expect } from 'chai';
|
|
4
|
+
import ContainerStorageService, {
|
|
5
|
+
absoluteStoragePaths,
|
|
6
|
+
assertSafeStoragePaths,
|
|
7
|
+
isSafeStoragePath,
|
|
8
|
+
containerStorageCommandsFactory,
|
|
9
|
+
containerStorageRootsFactory,
|
|
10
|
+
containerStorageSubtreeCommandsFactory,
|
|
11
|
+
coveringRootFactory,
|
|
12
|
+
ensureContainerStorage,
|
|
13
|
+
ensureContainerStorageSubtree,
|
|
14
|
+
localPathConfigCommandFactory,
|
|
15
|
+
localPathRootsFactory,
|
|
16
|
+
normalizeStorageRoots,
|
|
17
|
+
storageDirectoryCommandFactory,
|
|
18
|
+
} from '../../../../src/server/security/container-storage.js';
|
|
19
|
+
import { MongoBootstrap } from '../../../../src/db/mongo/MongoBootstrap.js';
|
|
20
|
+
|
|
21
|
+
const collect = (paths, options = {}) => {
|
|
22
|
+
const executed = [];
|
|
23
|
+
ensureContainerStorage(paths, { execute: (command) => executed.push(command), ...options });
|
|
24
|
+
return executed;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const mappedRoots = (commands) =>
|
|
28
|
+
commands
|
|
29
|
+
.filter((command) => command.includes('semanage fcontext -a'))
|
|
30
|
+
.map((command) => /semanage fcontext -a -t \S+ '([^']+)\(\/\.\*\)\?'/.exec(command)[1]);
|
|
31
|
+
|
|
32
|
+
const restoredPaths = (commands) => {
|
|
33
|
+
const restore = commands.find((command) => command.includes('restorecon'));
|
|
34
|
+
return restore ? [...restore.matchAll(/restorecon -RF '([^']+)'/g)].map((match) => match[1]) : [];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const createdPaths = (commands) =>
|
|
38
|
+
commands.filter((command) => command.startsWith('[ -d ')).map((command) => /^\[ -d '([^']+)'/.exec(command)[1]);
|
|
39
|
+
|
|
40
|
+
describe('Container storage SELinux preparation', () => {
|
|
41
|
+
it('exposes one canonical class API with named factory aliases', () => {
|
|
42
|
+
expect(ContainerStorageService.containerStorageCommandsFactory).to.equal(containerStorageCommandsFactory);
|
|
43
|
+
expect(ContainerStorageService.SHARED_CONTAINER_TYPE).to.equal('container_file_t');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('path normalization', () => {
|
|
47
|
+
it('keeps only absolute paths, deduplicated and ordered parents first', () => {
|
|
48
|
+
expect(absoluteStoragePaths(['/data/mongodb', '/data', 'relative/path', '', '/data/', null])).to.deep.equal([
|
|
49
|
+
'/data',
|
|
50
|
+
'/data/mongodb',
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('collapses descendants into the covering root', () => {
|
|
55
|
+
expect(normalizeStorageRoots(['/data/mongodb/v0', '/data', '/opt/local-path-provisioner'])).to.deep.equal([
|
|
56
|
+
'/data',
|
|
57
|
+
'/opt/local-path-provisioner',
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('never collapses a sibling that merely shares a prefix', () => {
|
|
62
|
+
expect(normalizeStorageRoots(['/data', '/database'])).to.deep.equal(['/data', '/database']);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('refuses to treat / as a storage root', () => {
|
|
66
|
+
expect(normalizeStorageRoots(['/'])).to.deep.equal([]);
|
|
67
|
+
expect(() => containerStorageCommandsFactory(['/'])).to.throw(TypeError);
|
|
68
|
+
expect(() => containerStorageCommandsFactory([])).to.throw(TypeError);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('resolves the longest declared root covering a path', () => {
|
|
72
|
+
expect(coveringRootFactory('/data/mongodb/v0')).to.equal('/data');
|
|
73
|
+
expect(coveringRootFactory('/data/mongodb/v0', ['/data', '/data/mongodb'])).to.equal('/data/mongodb');
|
|
74
|
+
expect(coveringRootFactory('/srv/unmapped')).to.equal('/srv/unmapped');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('system path protection', () => {
|
|
79
|
+
it('refuses a directory that is a system location in its own right', () => {
|
|
80
|
+
for (const path of ['/', '/opt', '/usr', '/var', '/home', '/etc', '/var/lib'])
|
|
81
|
+
expect(isSafeStoragePath(path), path).to.equal(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('refuses the CNI plugin directory, which a pod mounts but which holds executables', () => {
|
|
85
|
+
expect(isSafeStoragePath('/opt/cni/bin')).to.equal(false);
|
|
86
|
+
expect(isSafeStoragePath('/opt/cni')).to.equal(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('refuses anything inside a tree that can never hold container storage', () => {
|
|
90
|
+
for (const path of ['/usr/bin', '/etc/passwd', '/lib64/x', '/proc/1', '/sys/fs', '/boot/grub'])
|
|
91
|
+
expect(isSafeStoragePath(path), path).to.equal(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('allows a storage tree living under a system location', () => {
|
|
95
|
+
for (const path of ['/opt/local-path-provisioner', '/data', '/data/mongodb', '/home/dd/engine/volume/pv-x'])
|
|
96
|
+
expect(isSafeStoragePath(path), path).to.equal(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('allows only the hardcoded kubeadm control-plane mounts to break the prefix rule', () => {
|
|
100
|
+
for (const path of ContainerStorageService.CONTROL_PLANE_MOUNT_PATHS)
|
|
101
|
+
expect(isSafeStoragePath(path), path).to.equal(true);
|
|
102
|
+
expect(isSafeStoragePath('/etc/kubernetes-other')).to.equal(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('throws at the command boundary rather than emitting a relabel for a system path', () => {
|
|
106
|
+
expect(() => containerStorageCommandsFactory(['/data', '/opt/cni/bin'])).to.throw(RangeError, '/opt/cni/bin');
|
|
107
|
+
expect(() => containerStorageSubtreeCommandsFactory('/usr/bin')).to.throw(RangeError, '/usr/bin');
|
|
108
|
+
expect(() => assertSafeStoragePaths(['/opt'])).to.throw(RangeError);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('refuses a safe subtree whose covering root is not itself safe', () => {
|
|
112
|
+
expect(() => containerStorageSubtreeCommandsFactory('/opt/cni/bin/plugins', { roots: ['/opt/cni'] })).to.throw(
|
|
113
|
+
RangeError,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('keeps every declared platform root safe', () => {
|
|
118
|
+
for (const root of ContainerStorageService.PLATFORM_STORAGE_ROOTS)
|
|
119
|
+
expect(isSafeStoragePath(root), root).to.equal(true);
|
|
120
|
+
expect(isSafeStoragePath(ContainerStorageService.LOCAL_PATH_DEFAULT_ROOT)).to.equal(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('local-path provisioner discovery', () => {
|
|
125
|
+
it('reads the node roots out of a live provisioner config', () => {
|
|
126
|
+
const config = JSON.stringify({
|
|
127
|
+
nodePathMap: [
|
|
128
|
+
{ node: 'DEFAULT_PATH_FOR_NON_LISTED_NODES', paths: ['/opt/local-path-provisioner'] },
|
|
129
|
+
{ node: 'worker-1', paths: ['/srv/local-path', '/opt/local-path-provisioner'] },
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
expect(localPathRootsFactory(config)).to.deep.equal(['/opt/local-path-provisioner', '/srv/local-path']);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('reads a shared filesystem root', () => {
|
|
136
|
+
expect(localPathRootsFactory('{"sharedFileSystemPath":"/srv/shared"}')).to.deep.equal(['/srv/shared']);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('degrades to an empty result rather than throwing on absent or malformed config', () => {
|
|
140
|
+
expect(localPathRootsFactory('')).to.deep.equal([]);
|
|
141
|
+
expect(localPathRootsFactory('not json')).to.deep.equal([]);
|
|
142
|
+
expect(localPathRootsFactory('{"nodePathMap":"unexpected"}')).to.deep.equal([]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('falls back to the upstream default root when discovery found nothing', () => {
|
|
146
|
+
expect(containerStorageRootsFactory()).to.include(ContainerStorageService.LOCAL_PATH_DEFAULT_ROOT);
|
|
147
|
+
expect(containerStorageRootsFactory({ localPathRoots: ['/srv/local-path'] })).to.include('/srv/local-path');
|
|
148
|
+
expect(containerStorageRootsFactory({ localPathRoots: ['/srv/local-path'] })).to.not.include(
|
|
149
|
+
ContainerStorageService.LOCAL_PATH_DEFAULT_ROOT,
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('includes the control-plane mount paths only when asked', () => {
|
|
154
|
+
expect(containerStorageRootsFactory()).to.not.include('/var/lib/etcd');
|
|
155
|
+
expect(containerStorageRootsFactory({ controlPlane: true })).to.include('/var/lib/etcd');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('builds a namespace-scoped provisioner config lookup', () => {
|
|
159
|
+
expect(localPathConfigCommandFactory({ namespace: 'local-path-storage' })).to.include(
|
|
160
|
+
'kubectl get configmap local-path-config -n local-path-storage',
|
|
161
|
+
);
|
|
162
|
+
expect(() => localPathConfigCommandFactory({})).to.throw(TypeError);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('directory creation', () => {
|
|
167
|
+
it('creates a missing directory and leaves an existing one untouched', () => {
|
|
168
|
+
const command = storageDirectoryCommandFactory('/data/mysql');
|
|
169
|
+
expect(command).to.equal(`[ -d '/data/mysql' ] || sudo install -d -m 0755 '/data/mysql'`);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('applies an explicit mode and owner only at creation time', () => {
|
|
173
|
+
expect(storageDirectoryCommandFactory('/data/x', { mode: '0700', owner: '999:999' })).to.equal(
|
|
174
|
+
`[ -d '/data/x' ] || sudo install -d -m 0700 -o '999' -g '999' '/data/x'`,
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('quotes paths containing shell metacharacters', () => {
|
|
179
|
+
expect(storageDirectoryCommandFactory("/data/o'hara")).to.include(`'/data/o'"'"'hara'`);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('requires a path', () => {
|
|
183
|
+
expect(() => storageDirectoryCommandFactory('')).to.throw(TypeError);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('fresh deployment', () => {
|
|
188
|
+
const commands = collect(['/data', '/data/mongodb', '/data/mongodb/v0', '/opt/local-path-provisioner']);
|
|
189
|
+
|
|
190
|
+
it('creates every requested directory, including the nested volume directories', () => {
|
|
191
|
+
expect(createdPaths(commands)).to.deep.equal([
|
|
192
|
+
'/data',
|
|
193
|
+
'/data/mongodb',
|
|
194
|
+
'/data/mongodb/v0',
|
|
195
|
+
'/opt/local-path-provisioner',
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('registers one persistent mapping per covering root, not per volume', () => {
|
|
200
|
+
expect(mappedRoots(commands)).to.deep.equal(['/data', '/opt/local-path-provisioner']);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('creates directories before labeling them, so nothing is relabeled out from under itself', () => {
|
|
204
|
+
const firstMapping = commands.findIndex((command) => command.includes('semanage fcontext'));
|
|
205
|
+
const lastCreate = commands.map((command) => command.startsWith('[ -d ')).lastIndexOf(true);
|
|
206
|
+
expect(lastCreate).to.be.lessThan(firstMapping);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('restores the covering roots last, so the mapping is already in the store', () => {
|
|
210
|
+
expect(restoredPaths(commands)).to.deep.equal(['/data', '/opt/local-path-provisioner']);
|
|
211
|
+
expect(commands[commands.length - 1]).to.include('restorecon');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('uses semanage rather than chcon, so the label survives a policy relabel', () => {
|
|
215
|
+
expect(commands.join('\n')).to.include('semanage fcontext');
|
|
216
|
+
expect(commands.join('\n')).to.not.include('chcon');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('reports actionable remediation instead of silently skipping when semanage is absent', () => {
|
|
220
|
+
expect(commands.find((command) => command.includes('semanage fcontext'))).to.include(
|
|
221
|
+
'semanage is required for persistent file contexts',
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('no-ops entirely on a host without SELinux', () => {
|
|
226
|
+
for (const command of commands.filter((entry) => entry.includes('semanage')))
|
|
227
|
+
expect(command).to.include('selinuxenabled');
|
|
228
|
+
for (const command of commands.filter((entry) => entry.includes('restorecon')))
|
|
229
|
+
expect(command).to.include('command -v restorecon');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('redeployment', () => {
|
|
234
|
+
it('emits an identical command list on a repeat run', () => {
|
|
235
|
+
const first = collect(['/data', '/opt/local-path-provisioner']);
|
|
236
|
+
const second = collect(['/data', '/opt/local-path-provisioner']);
|
|
237
|
+
expect(second).to.deep.equal(first);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('is order-independent, so a caller cannot change the applied state by reordering', () => {
|
|
241
|
+
expect(collect(['/opt/local-path-provisioner', '/data'])).to.deep.equal(
|
|
242
|
+
collect(['/data', '/opt/local-path-provisioner']),
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('adds the mapping when absent and amends it when present, never failing on either', () => {
|
|
247
|
+
const mapping = collect(['/data']).find((command) => command.includes('semanage fcontext'));
|
|
248
|
+
expect(mapping).to.include('semanage fcontext -a -t container_file_t');
|
|
249
|
+
expect(mapping).to.include('semanage fcontext -m -t container_file_t');
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('existing persistent data', () => {
|
|
254
|
+
it('never re-modes, re-owns or removes a directory that already exists', () => {
|
|
255
|
+
const commands = collect(['/data/mongodb/v0']);
|
|
256
|
+
expect(commands.filter((command) => /(^|[^-])\bchmod\b/.test(command))).to.deep.equal([]);
|
|
257
|
+
expect(commands.filter((command) => /\bchown\b/.test(command))).to.deep.equal([]);
|
|
258
|
+
expect(commands.filter((command) => /\brm\b/.test(command))).to.deep.equal([]);
|
|
259
|
+
expect(commands.filter((command) => command.startsWith('[ -d '))).to.have.length(1);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('relabels a data subtree without registering a mapping for it', () => {
|
|
263
|
+
const commands = containerStorageSubtreeCommandsFactory('/home/dd/engine/volume/pv-app-production-blue');
|
|
264
|
+
expect(mappedRoots(commands)).to.deep.equal(['/home/dd/engine/volume']);
|
|
265
|
+
expect(restoredPaths(commands)).to.deep.equal(['/home/dd/engine/volume/pv-app-production-blue']);
|
|
266
|
+
expect(createdPaths(commands)).to.deep.equal([]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('maps an uncovered subtree at its own path rather than leaving it unmapped', () => {
|
|
270
|
+
expect(mappedRoots(containerStorageSubtreeCommandsFactory('/srv/external-volume'))).to.deep.equal([
|
|
271
|
+
'/srv/external-volume',
|
|
272
|
+
]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('collapses many volumes of one root into a single mapping and a single restore', () => {
|
|
276
|
+
const commands = containerStorageSubtreeCommandsFactory([
|
|
277
|
+
'/home/dd/engine/volume/pv-a',
|
|
278
|
+
'/home/dd/engine/volume/pv-b',
|
|
279
|
+
'/home/dd/engine/volume',
|
|
280
|
+
]);
|
|
281
|
+
expect(mappedRoots(commands)).to.deep.equal(['/home/dd/engine/volume']);
|
|
282
|
+
expect(restoredPaths(commands)).to.deep.equal(['/home/dd/engine/volume']);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('skips a restore whose target does not exist instead of failing the deploy', () => {
|
|
286
|
+
expect(restoredPaths(containerStorageSubtreeCommandsFactory('/data/absent'))).to.deep.equal(['/data/absent']);
|
|
287
|
+
const restore = containerStorageSubtreeCommandsFactory('/data/absent').find((command) =>
|
|
288
|
+
command.includes('restorecon'),
|
|
289
|
+
);
|
|
290
|
+
expect(restore).to.include(`[ ! -e '/data/absent' ] ||`);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe('failure and recovery paths', () => {
|
|
295
|
+
it('requires an executor rather than silently doing nothing', () => {
|
|
296
|
+
expect(() => ensureContainerStorage('/data', {})).to.throw(TypeError);
|
|
297
|
+
expect(() => ensureContainerStorageSubtree('/data', {})).to.throw(TypeError);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('rejects an empty or relative target', () => {
|
|
301
|
+
expect(() => containerStorageSubtreeCommandsFactory([])).to.throw(TypeError);
|
|
302
|
+
expect(() => containerStorageSubtreeCommandsFactory(['relative'])).to.throw(TypeError);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('drops sudo when already running as root on the node', () => {
|
|
306
|
+
const commands = containerStorageCommandsFactory(['/data'], { sudo: false });
|
|
307
|
+
expect(commands.join('\n')).to.not.include('sudo ');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('propagates an executor failure instead of reporting success', () => {
|
|
311
|
+
expect(() =>
|
|
312
|
+
ensureContainerStorage('/data', {
|
|
313
|
+
execute: () => {
|
|
314
|
+
throw new Error('semanage failed');
|
|
315
|
+
},
|
|
316
|
+
}),
|
|
317
|
+
).to.throw('semanage failed');
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('MongoDB replica data root', () => {
|
|
322
|
+
it('prepares the root and one directory per member', () => {
|
|
323
|
+
expect(MongoBootstrap.prepareReplicaDataRoot(3, { execute: () => {} })).to.deep.equal([
|
|
324
|
+
'/data/mongodb',
|
|
325
|
+
'/data/mongodb/v0',
|
|
326
|
+
'/data/mongodb/v1',
|
|
327
|
+
'/data/mongodb/v2',
|
|
328
|
+
]);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('creates every member directory but maps only /data', () => {
|
|
332
|
+
const executed = [];
|
|
333
|
+
MongoBootstrap.prepareReplicaDataRoot(3, { execute: (command) => executed.push(command) });
|
|
334
|
+
expect(createdPaths(executed)).to.deep.equal([
|
|
335
|
+
'/data/mongodb',
|
|
336
|
+
'/data/mongodb/v0',
|
|
337
|
+
'/data/mongodb/v1',
|
|
338
|
+
'/data/mongodb/v2',
|
|
339
|
+
]);
|
|
340
|
+
expect(mappedRoots(executed)).to.deep.equal(['/data']);
|
|
341
|
+
expect(restoredPaths(executed)).to.deep.equal(['/data/mongodb']);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('leaves ownership and mode of an existing member directory to the init container', () => {
|
|
345
|
+
const executed = [];
|
|
346
|
+
MongoBootstrap.prepareReplicaDataRoot(1, { execute: (command) => executed.push(command) });
|
|
347
|
+
expect(executed.join('\n')).to.not.include('chown');
|
|
348
|
+
expect(executed.join('\n')).to.not.match(/(^|\s)chmod\s/);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('tolerates a zero or unparsable replica count without emitting a bad path', () => {
|
|
352
|
+
expect(MongoBootstrap.prepareReplicaDataRoot(0, { execute: () => {} })).to.deep.equal(['/data/mongodb']);
|
|
353
|
+
expect(MongoBootstrap.prepareReplicaDataRoot(undefined, { execute: () => {} })).to.deep.equal(['/data/mongodb']);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|