@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,902 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { expect } from 'chai';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import { cronJobYamlFactory } from '../../../../src/server/ops/cron.js';
|
|
6
|
+
import {
|
|
7
|
+
UNDERPOST_MONITORING,
|
|
8
|
+
alertRulesFactory,
|
|
9
|
+
deployedEventIdsFactory,
|
|
10
|
+
nodeExporterManifestFactory,
|
|
11
|
+
nodeExporterServiceFactory,
|
|
12
|
+
nodeExporterServiceScriptFactory,
|
|
13
|
+
nodeMetricsDashboardFactory,
|
|
14
|
+
alertmanagerConfFactory,
|
|
15
|
+
appScrapeEntriesFactory,
|
|
16
|
+
blackboxConfFactory,
|
|
17
|
+
envoyDashboardFactory,
|
|
18
|
+
eventsDashboardFactory,
|
|
19
|
+
grafanaAdminSecretFactory,
|
|
20
|
+
grafanaDeploymentFactory,
|
|
21
|
+
grafanaDeploymentPatchFactory,
|
|
22
|
+
grafanaExposureFactory,
|
|
23
|
+
grafanaNodePortFactory,
|
|
24
|
+
grafanaResetPlanFactory,
|
|
25
|
+
grafanaStorageResetRequiredFactory,
|
|
26
|
+
metricsPathFactory,
|
|
27
|
+
monitoringConfigFactory,
|
|
28
|
+
probeGroupsFactory,
|
|
29
|
+
prometheusConfFactory,
|
|
30
|
+
scrapeDeployListFactory,
|
|
31
|
+
serviceDnsFactory,
|
|
32
|
+
webhookSecretFactory,
|
|
33
|
+
} from '../../../../src/server/ops/monitoring.js';
|
|
34
|
+
import { hubTunnelAddressFactory } from '../../../../src/cli/wireguard.js';
|
|
35
|
+
|
|
36
|
+
// A deploy that mixes every runtime shape the router can carry: two scrapeable
|
|
37
|
+
// Express paths, one non-Express runtime, and one redirect-only entry.
|
|
38
|
+
const CONF_SERVER = {
|
|
39
|
+
'www.example.com': {
|
|
40
|
+
'/': { runtime: 'nodejs' },
|
|
41
|
+
'/api': { runtime: 'nodejs' },
|
|
42
|
+
'/legacy': { runtime: 'lampp' },
|
|
43
|
+
},
|
|
44
|
+
'old.example.com': {
|
|
45
|
+
'/': { runtime: 'nodejs', redirect: 'https://www.example.com' },
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const PROBES = [
|
|
50
|
+
{ eventId: 'wireguard-server-down', module: 'icmp', targets: ['10.0.0.1'], labels: { underpost_role: 'hub' } },
|
|
51
|
+
{
|
|
52
|
+
eventId: 'wireguard-spoke-down',
|
|
53
|
+
module: 'icmp',
|
|
54
|
+
targets: ['10.0.0.2'],
|
|
55
|
+
labels: { underpost_role: 'spoke', underpost_spoke: 'node-a' },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
eventId: 'wireguard-spoke-down',
|
|
59
|
+
module: 'icmp',
|
|
60
|
+
targets: ['10.0.0.3'],
|
|
61
|
+
labels: { underpost_role: 'spoke', underpost_spoke: 'node-b' },
|
|
62
|
+
},
|
|
63
|
+
{ eventId: 'other-down', module: 'tcp_connect', targets: ['1.2.3.4:443'] },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const docs = (yaml) => yaml.split('\n---\n').filter((doc) => doc.trim());
|
|
67
|
+
const kind = (yaml, name) => docs(yaml).find((doc) => doc.includes(`\nkind: ${name}\n`));
|
|
68
|
+
const configMap = (yaml, name) => docs(yaml).find((doc) => doc.includes(` name: ${name}\n`));
|
|
69
|
+
|
|
70
|
+
describe('metricsPathFactory', () => {
|
|
71
|
+
it('mounts the root runtime at /metrics and a sub-path under its own prefix', () => {
|
|
72
|
+
expect(metricsPathFactory('/')).to.equal('/metrics');
|
|
73
|
+
expect(metricsPathFactory('/api')).to.equal('/api/metrics');
|
|
74
|
+
expect(metricsPathFactory()).to.equal('/metrics');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('appScrapeEntriesFactory', () => {
|
|
79
|
+
it('selects only the Express runtimes that actually serve a registry', () => {
|
|
80
|
+
const entries = appScrapeEntriesFactory(CONF_SERVER);
|
|
81
|
+
expect(entries).to.deep.equal([
|
|
82
|
+
{ host: 'www.example.com', metricsPath: '/metrics', scheme: 'https' },
|
|
83
|
+
{ host: 'www.example.com', metricsPath: '/api/metrics', scheme: 'https' },
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('scrapes over http in development', () => {
|
|
88
|
+
expect(appScrapeEntriesFactory(CONF_SERVER, { scheme: 'http' }).every((e) => e.scheme === 'http')).to.equal(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('treats an empty configuration as no targets', () => {
|
|
92
|
+
expect(appScrapeEntriesFactory({})).to.deep.equal([]);
|
|
93
|
+
expect(appScrapeEntriesFactory()).to.deep.equal([]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('scrapeDeployListFactory', () => {
|
|
98
|
+
const routers = { cronDeployId: 'dd-cron', routerDeployIds: ['dd-core', 'dd-cyberia'] };
|
|
99
|
+
|
|
100
|
+
it('defaults to the cron deploy plus the router deploys, the set loadCronDeployEnv loads', () => {
|
|
101
|
+
expect(scrapeDeployListFactory(routers)).to.deep.equal(['dd-cron', 'dd-core', 'dd-cyberia']);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('treats the dd meta id the same as no id at all', () => {
|
|
105
|
+
expect(scrapeDeployListFactory({ ...routers, deployId: 'dd' })).to.deep.equal(scrapeDeployListFactory(routers));
|
|
106
|
+
// `dd` means "everything", so it wins over any id listed beside it.
|
|
107
|
+
expect(scrapeDeployListFactory({ ...routers, deployId: 'dd,dd-other' })).to.deep.equal(
|
|
108
|
+
scrapeDeployListFactory(routers),
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('narrows to an explicit id or list without re-adding the defaults', () => {
|
|
113
|
+
expect(scrapeDeployListFactory({ ...routers, deployId: 'dd-cyberia' })).to.deep.equal(['dd-cyberia']);
|
|
114
|
+
expect(scrapeDeployListFactory({ ...routers, deployId: 'dd-core, dd-test' })).to.deep.equal(['dd-core', 'dd-test']);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('never scrapes one deploy twice when the cron deploy is also in the router', () => {
|
|
118
|
+
expect(scrapeDeployListFactory({ cronDeployId: 'dd-core', routerDeployIds: ['dd-core', 'dd-core'] })).to.deep.equal(
|
|
119
|
+
['dd-core'],
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('resolves to nothing rather than guessing when neither router file has content', () => {
|
|
124
|
+
expect(scrapeDeployListFactory({})).to.deep.equal([]);
|
|
125
|
+
expect(scrapeDeployListFactory()).to.deep.equal([]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('probeGroupsFactory', () => {
|
|
130
|
+
it('groups probes by module so one job serves every event probing that way', () => {
|
|
131
|
+
const groups = probeGroupsFactory(PROBES);
|
|
132
|
+
expect(groups.map((group) => group.module)).to.deep.equal(['icmp', 'tcp_connect']);
|
|
133
|
+
expect(groups[0].groups).to.have.length(3);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('keeps each probe its own labelled group, so a fan-out event names its subject', () => {
|
|
137
|
+
const [icmp] = probeGroupsFactory(PROBES);
|
|
138
|
+
expect(icmp.groups.map((group) => group.labels)).to.deep.equal([
|
|
139
|
+
{ underpost_event: 'wireguard-server-down', underpost_role: 'hub' },
|
|
140
|
+
{ underpost_event: 'wireguard-spoke-down', underpost_role: 'spoke', underpost_spoke: 'node-a' },
|
|
141
|
+
{ underpost_event: 'wireguard-spoke-down', underpost_role: 'spoke', underpost_spoke: 'node-b' },
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('always labels a probe with its event, even when it carries no labels of its own', () => {
|
|
146
|
+
const [group] = probeGroupsFactory([{ eventId: 'solo', module: 'icmp', targets: ['1.1.1.1'] }]);
|
|
147
|
+
expect(group.groups[0].labels).to.deep.equal({ underpost_event: 'solo' });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('drops probes that carry no target, module, or event', () => {
|
|
151
|
+
expect(
|
|
152
|
+
probeGroupsFactory([
|
|
153
|
+
{ eventId: 'a', module: 'icmp', targets: [] },
|
|
154
|
+
{ eventId: '', module: 'icmp', targets: ['1.1.1.1'] },
|
|
155
|
+
{ eventId: 'a', module: '', targets: ['1.1.1.1'] },
|
|
156
|
+
]),
|
|
157
|
+
).to.deep.equal([]);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('prometheusConfFactory', () => {
|
|
162
|
+
const conf = prometheusConfFactory({
|
|
163
|
+
appTargets: appScrapeEntriesFactory(CONF_SERVER),
|
|
164
|
+
extraTargets: ['127.0.0.1:9100'],
|
|
165
|
+
probes: PROBES,
|
|
166
|
+
namespace: 'obs',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('gives each distinct metrics path its own job', () => {
|
|
170
|
+
expect(conf).to.include("job_name: 'underpost-express'");
|
|
171
|
+
expect(conf).to.include("job_name: 'underpost-express-1'");
|
|
172
|
+
expect(conf).to.include('metrics_path: /api/metrics');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('discovers the Envoy data plane instead of naming its generated Deployment', () => {
|
|
176
|
+
expect(conf).to.include('kubernetes_sd_configs');
|
|
177
|
+
expect(conf).to.include(`names: ['${UNDERPOST_MONITORING.envoy.namespace}']`);
|
|
178
|
+
expect(conf).to.include(`replacement: '$1:${UNDERPOST_MONITORING.envoy.metricsPort}'`);
|
|
179
|
+
expect(conf).to.include(`metrics_path: ${UNDERPOST_MONITORING.envoy.metricsPath}`);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('labels every probe target with the event whose rule selects on it', () => {
|
|
183
|
+
expect(conf).to.include("underpost_event: 'wireguard-server-down'");
|
|
184
|
+
expect(conf).to.include("underpost_event: 'other-down'");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('carries the spoke id into the scrape config, so the alert can name it', () => {
|
|
188
|
+
expect(conf).to.include("underpost_spoke: 'node-a'");
|
|
189
|
+
expect(conf).to.include("underpost_spoke: 'node-b'");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('addresses the exporter and Alertmanager by namespace-qualified name', () => {
|
|
193
|
+
expect(conf).to.include(
|
|
194
|
+
serviceDnsFactory(UNDERPOST_MONITORING.blackbox.name, UNDERPOST_MONITORING.blackbox.port, 'obs'),
|
|
195
|
+
);
|
|
196
|
+
expect(conf).to.include(
|
|
197
|
+
serviceDnsFactory(UNDERPOST_MONITORING.alertmanager.name, UNDERPOST_MONITORING.alertmanager.port, 'obs'),
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('omits the extra-target job when nothing extra is requested', () => {
|
|
202
|
+
expect(prometheusConfFactory({})).to.not.include('underpost-extra');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('alertRulesFactory', () => {
|
|
207
|
+
const events = [
|
|
208
|
+
{
|
|
209
|
+
id: 'wireguard-server-down',
|
|
210
|
+
description: 'hub down',
|
|
211
|
+
alert: { name: 'UnderpostWireguardServerDown', expr: 'probe_success == 0', for: '2m' },
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: 'wireguard-spoke-down',
|
|
215
|
+
description: 'spoke down',
|
|
216
|
+
alert: {
|
|
217
|
+
name: 'UnderpostWireguardSpokeDown',
|
|
218
|
+
expr: 'probe_success == 0',
|
|
219
|
+
for: '2m',
|
|
220
|
+
summary: 'spoke {{ $labels.underpost_spoke }} unreachable',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{ id: 'no-rule', description: 'nothing to alert on' },
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
it('emits a rule per event that defines one, always labelled with its event id', () => {
|
|
227
|
+
const rules = alertRulesFactory(events);
|
|
228
|
+
expect(rules).to.include('alert: UnderpostWireguardServerDown');
|
|
229
|
+
expect(rules).to.include('alert: UnderpostWireguardSpokeDown');
|
|
230
|
+
expect(rules).to.include("underpost_event: 'wireguard-server-down'");
|
|
231
|
+
expect(rules).to.include("underpost_event: 'wireguard-spoke-down'");
|
|
232
|
+
expect(rules).to.include('severity: critical');
|
|
233
|
+
expect(rules).to.not.include('no-rule');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('keeps the subject label template in the summary, so a spoke alert names the spoke', () => {
|
|
237
|
+
expect(alertRulesFactory(events)).to.include('{{ $labels.underpost_spoke }}');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('skips an event whose alert window the contract never declared', () => {
|
|
241
|
+
const rules = alertRulesFactory([{ id: 'undeclared', alert: { name: 'UnderpostUndeclared', expr: 'up == 0' } }]);
|
|
242
|
+
expect(rules).to.not.include('UnderpostUndeclared');
|
|
243
|
+
expect(rules).to.not.include('for: 2m');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('renders an empty rule list rather than invalid YAML', () => {
|
|
247
|
+
expect(alertRulesFactory([])).to.include('rules:\n []');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('alertmanagerConfFactory', () => {
|
|
252
|
+
const conf = alertmanagerConfFactory({ webhookUrl: 'http://10.0.0.5:9099/event' });
|
|
253
|
+
|
|
254
|
+
it('groups by subject as well as by event, so one webhook covers one spoke', () => {
|
|
255
|
+
expect(conf).to.include("group_by: ['alertname', 'underpost_event', 'underpost_spoke']");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('routes only labelled alerts to the dispatcher and everything else nowhere', () => {
|
|
259
|
+
expect(conf).to.include('underpost_event =~ ".+"');
|
|
260
|
+
expect(conf).to.include("receiver: 'underpost-null'");
|
|
261
|
+
expect(conf).to.include("url: 'http://10.0.0.5:9099/event'");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('never delivers a resolution notice, which would re-run remediation', () => {
|
|
265
|
+
expect(conf).to.include('send_resolved: false');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('always presents the mounted bearer token', () => {
|
|
269
|
+
expect(conf).to.include(`credentials_file: /etc/alertmanager/secret/${UNDERPOST_MONITORING.alertmanager.tokenKey}`);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('blackboxConfFactory', () => {
|
|
274
|
+
it('defines exactly the modules the probes name', () => {
|
|
275
|
+
const conf = blackboxConfFactory();
|
|
276
|
+
for (const module of ['http_2xx', 'tcp_connect', 'icmp']) expect(conf).to.include(`${module}:`);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('monitoringConfigFactory', () => {
|
|
281
|
+
const yaml = monitoringConfigFactory({
|
|
282
|
+
namespace: 'obs',
|
|
283
|
+
prometheusConf: 'global:\n scrape_interval: 30s\n',
|
|
284
|
+
alertRules: 'groups: []\n',
|
|
285
|
+
alertmanagerConf: 'route:\n receiver: x\n',
|
|
286
|
+
blackboxConf: 'modules: {}\n',
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('binds the pod-discovery ClusterRoleBinding to the namespace it renders into', () => {
|
|
290
|
+
const binding = kind(yaml, 'ClusterRoleBinding');
|
|
291
|
+
expect(binding).to.include('namespace: obs');
|
|
292
|
+
expect(binding).to.include(`name: ${UNDERPOST_MONITORING.prometheus.name}`);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('emits every ConfigMap the four components mount', () => {
|
|
296
|
+
for (const name of [
|
|
297
|
+
UNDERPOST_MONITORING.prometheus.configMapName,
|
|
298
|
+
UNDERPOST_MONITORING.prometheus.rulesConfigMapName,
|
|
299
|
+
UNDERPOST_MONITORING.alertmanager.configMapName,
|
|
300
|
+
UNDERPOST_MONITORING.blackbox.configMapName,
|
|
301
|
+
UNDERPOST_MONITORING.grafana.datasourcesConfigMapName,
|
|
302
|
+
UNDERPOST_MONITORING.grafana.dashboardProviderConfigMapName,
|
|
303
|
+
UNDERPOST_MONITORING.grafana.dashboardsConfigMapName,
|
|
304
|
+
])
|
|
305
|
+
expect(configMap(yaml, name), name).to.be.a('string');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('indents each embedded file under its block scalar', () => {
|
|
309
|
+
expect(configMap(yaml, UNDERPOST_MONITORING.prometheus.configMapName)).to.include(
|
|
310
|
+
' prometheus.yml: |\n global:\n scrape_interval: 30s',
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('points the provisioned datasource at Prometheus in the same namespace', () => {
|
|
315
|
+
expect(configMap(yaml, UNDERPOST_MONITORING.grafana.datasourcesConfigMapName)).to.include(
|
|
316
|
+
serviceDnsFactory(UNDERPOST_MONITORING.prometheus.name, UNDERPOST_MONITORING.prometheus.port, 'obs'),
|
|
317
|
+
);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('never renders Grafana administrator credentials into a ConfigMap', () => {
|
|
321
|
+
expect(yaml).to.not.include('GF_SECURITY_ADMIN_PASSWORD');
|
|
322
|
+
expect(yaml).to.not.include('admin-password');
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('grafanaAdminSecretFactory', () => {
|
|
327
|
+
it('renders credentials only as Secret data', () => {
|
|
328
|
+
const secret = grafanaAdminSecretFactory({ username: 'operator', password: 'S3cret!', namespace: 'obs' });
|
|
329
|
+
expect(secret).to.include('kind: Secret');
|
|
330
|
+
expect(secret).to.include('name: grafana-admin');
|
|
331
|
+
expect(secret).to.include(`admin-user: ${Buffer.from('operator').toString('base64')}`);
|
|
332
|
+
expect(secret).to.include(`admin-password: ${Buffer.from('S3cret!').toString('base64')}`);
|
|
333
|
+
expect(secret).to.not.include('S3cret!');
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe('webhookSecretFactory', () => {
|
|
338
|
+
it('base64-encodes the token under the key Alertmanager reads as a file', () => {
|
|
339
|
+
const secret = webhookSecretFactory({ token: 'shhh', namespace: 'obs' });
|
|
340
|
+
expect(secret).to.include(
|
|
341
|
+
`${UNDERPOST_MONITORING.alertmanager.tokenKey}: ${Buffer.from('shhh').toString('base64')}`,
|
|
342
|
+
);
|
|
343
|
+
expect(secret).to.include('namespace: obs');
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe('grafanaExposureFactory', () => {
|
|
348
|
+
it('serves from the sub-path behind the edge, keeping the prefix in the root URL', () => {
|
|
349
|
+
// With serve_from_sub_path Grafana expects to receive /grafana, so the route
|
|
350
|
+
// must not rewrite it away.
|
|
351
|
+
expect(grafanaExposureFactory({ host: 'www.example.com' })).to.deep.equal({
|
|
352
|
+
rootUrl: 'https://www.example.com/grafana/',
|
|
353
|
+
subPath: true,
|
|
354
|
+
url: 'https://www.example.com/grafana',
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('owns the whole origin on a NodePort, so it serves from the root', () => {
|
|
359
|
+
expect(grafanaExposureFactory({ nodeIp: '192.168.1.85', nodePort: 32300 })).to.deep.equal({
|
|
360
|
+
rootUrl: 'http://192.168.1.85:32300/',
|
|
361
|
+
subPath: false,
|
|
362
|
+
url: 'http://192.168.1.85:32300',
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('prefers the edge host when both exposures are published', () => {
|
|
367
|
+
const exposure = grafanaExposureFactory({ host: 'www.example.com', nodeIp: '192.168.1.85' });
|
|
368
|
+
expect(exposure.subPath).to.equal(true);
|
|
369
|
+
expect(exposure.url).to.equal('https://www.example.com/grafana');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('falls back to the port-forward origin when nothing is published', () => {
|
|
373
|
+
expect(grafanaExposureFactory({})).to.deep.equal({
|
|
374
|
+
rootUrl: 'http://localhost:3000/',
|
|
375
|
+
subPath: false,
|
|
376
|
+
url: 'http://localhost:3000',
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
describe('grafana rollout constraints', () => {
|
|
382
|
+
it('rolls Grafana with Recreate so one process owns its data directory', () => {
|
|
383
|
+
const deployment = fs.readFileSync('./manifests/grafana/deployment.yaml', 'utf8');
|
|
384
|
+
const pvc = fs.readFileSync('./manifests/grafana/pvc.yaml', 'utf8');
|
|
385
|
+
expect(pvc).to.include('ReadWriteOnce');
|
|
386
|
+
expect(deployment).to.include('strategy:');
|
|
387
|
+
expect(deployment).to.include('type: Recreate');
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('keeps the URL model out of the manifest, so the exposure owns it', () => {
|
|
391
|
+
const deployment = fs.readFileSync('./manifests/grafana/deployment.yaml', 'utf8');
|
|
392
|
+
expect(deployment).to.not.include('GF_SERVER_ROOT_URL:');
|
|
393
|
+
expect(deployment).to.not.include('GF_SERVER_SERVE_FROM_SUB_PATH:');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('publishes browser access separately from the datasource Service', () => {
|
|
397
|
+
expect(fs.readFileSync('./manifests/grafana/service.yaml', 'utf8')).to.include('type: ClusterIP');
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('resets the persisted admin through stdin against Grafana container paths', () => {
|
|
401
|
+
const monitor = fs.readFileSync('./src/cli/monitor.js', 'utf8');
|
|
402
|
+
expect(monitor).to.include('admin reset-admin-password --password-from-stdin');
|
|
403
|
+
expect(monitor).to.include('GF_PATHS_CONFIG:-/etc/grafana/grafana.ini');
|
|
404
|
+
expect(monitor).to.include('GF_PATHS_DATA:-/var/lib/grafana');
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
describe('grafanaDeploymentFactory', () => {
|
|
409
|
+
const deployment = {
|
|
410
|
+
metadata: { name: 'grafana' },
|
|
411
|
+
spec: {
|
|
412
|
+
template: {
|
|
413
|
+
spec: {
|
|
414
|
+
containers: [{ name: 'grafana', env: [{ name: 'GF_SECURITY_ADMIN_USER', value: 'operator' }] }],
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
const exposure = { rootUrl: 'http://192.168.1.85:32300/', subPath: false };
|
|
420
|
+
|
|
421
|
+
it('places Grafana and configures its origin before the Deployment is applied', () => {
|
|
422
|
+
const rendered = grafanaDeploymentFactory({
|
|
423
|
+
deployment,
|
|
424
|
+
exposure,
|
|
425
|
+
namespace: 'obs',
|
|
426
|
+
nodeName: 'node-a',
|
|
427
|
+
adminSecretVersion: '42',
|
|
428
|
+
});
|
|
429
|
+
expect(rendered.metadata.namespace).to.equal('obs');
|
|
430
|
+
expect(rendered.spec.template.spec.nodeSelector).to.deep.equal({ 'kubernetes.io/hostname': 'node-a' });
|
|
431
|
+
expect(rendered.spec.template.spec.containers[0].env).to.deep.equal([
|
|
432
|
+
{
|
|
433
|
+
name: 'GF_SECURITY_ADMIN_USER',
|
|
434
|
+
valueFrom: { secretKeyRef: { name: 'grafana-admin', key: 'admin-user' } },
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
name: 'GF_SECURITY_ADMIN_PASSWORD',
|
|
438
|
+
valueFrom: { secretKeyRef: { name: 'grafana-admin', key: 'admin-password' } },
|
|
439
|
+
},
|
|
440
|
+
{ name: 'GF_SERVER_ROOT_URL', value: exposure.rootUrl },
|
|
441
|
+
{ name: 'GF_SERVER_SERVE_FROM_SUB_PATH', value: 'false' },
|
|
442
|
+
]);
|
|
443
|
+
expect(rendered.spec.template.spec.containers[0].env.map(({ name }) => name)).to.not.include(
|
|
444
|
+
'GF_SECURITY_ADMIN_EMAIL',
|
|
445
|
+
);
|
|
446
|
+
expect(rendered.spec.template.metadata.annotations).to.deep.equal({
|
|
447
|
+
'underpost.net/grafana-admin-secret-version': '42',
|
|
448
|
+
});
|
|
449
|
+
expect(deployment.metadata).to.not.have.property('namespace');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('uses the same runtime fields when republishing an existing Deployment', () => {
|
|
453
|
+
const patch = grafanaDeploymentPatchFactory({ exposure, nodeName: 'node-a' });
|
|
454
|
+
expect(patch.spec.template.spec.nodeSelector).to.deep.equal({ 'kubernetes.io/hostname': 'node-a' });
|
|
455
|
+
expect(patch.spec.template.spec.containers[0].env).to.deep.equal([
|
|
456
|
+
{ name: 'GF_SERVER_ROOT_URL', value: exposure.rootUrl },
|
|
457
|
+
{ name: 'GF_SERVER_SERVE_FROM_SUB_PATH', value: 'false' },
|
|
458
|
+
]);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('adds Secret references and its rollout version during credential convergence', () => {
|
|
462
|
+
const patch = grafanaDeploymentPatchFactory({ exposure, adminSecretVersion: '43' });
|
|
463
|
+
expect(patch.spec.template.metadata.annotations).to.deep.equal({
|
|
464
|
+
'underpost.net/grafana-admin-secret-version': '43',
|
|
465
|
+
});
|
|
466
|
+
expect(patch.spec.template.spec.containers[0].env.slice(0, 2)).to.deep.equal(
|
|
467
|
+
grafanaDeploymentFactory({ deployment, exposure }).spec.template.spec.containers[0].env.slice(0, 2),
|
|
468
|
+
);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
describe('grafanaStorageResetRequiredFactory', () => {
|
|
473
|
+
it('does not reset storage before the PVC is bound', () => {
|
|
474
|
+
expect(grafanaStorageResetRequiredFactory({ nodeName: 'control', volumeNodes: [] })).to.equal(false);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('does not reset storage without an explicit destination node', () => {
|
|
478
|
+
expect(grafanaStorageResetRequiredFactory({ volumeNodes: ['worker'] })).to.equal(false);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('resets storage when the requested node differs from its owner', () => {
|
|
482
|
+
expect(grafanaStorageResetRequiredFactory({ nodeName: 'control', volumeNodes: ['worker'] })).to.equal(true);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('preserves storage already bound to the requested node', () => {
|
|
486
|
+
expect(grafanaStorageResetRequiredFactory({ nodeName: 'worker', volumeNodes: ['worker'] })).to.equal(false);
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe('grafanaResetPlanFactory', () => {
|
|
491
|
+
it('covers every workload, exposure, and storage resource owned by Grafana', () => {
|
|
492
|
+
expect(grafanaResetPlanFactory('pvc-volume')).to.deep.equal({
|
|
493
|
+
controllers: ['deployment/grafana', 'statefulset/grafana'],
|
|
494
|
+
resources: ['service/grafana', 'service/grafana-nodeport', 'httproute/grafana-route'],
|
|
495
|
+
claim: 'pvc/grafana-pvc',
|
|
496
|
+
volumeName: 'pvc-volume',
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
describe('grafanaNodePortFactory', () => {
|
|
502
|
+
it('publishes a second Service, leaving the ClusterIP the datasource resolves intact', () => {
|
|
503
|
+
const yaml = grafanaNodePortFactory({ namespace: 'obs' });
|
|
504
|
+
expect(yaml).to.include(`name: ${UNDERPOST_MONITORING.grafana.nodePortName}`);
|
|
505
|
+
expect(yaml).to.include('type: NodePort');
|
|
506
|
+
expect(yaml).to.include(`nodePort: ${UNDERPOST_MONITORING.grafana.nodePort}`);
|
|
507
|
+
expect(yaml).to.include('namespace: obs');
|
|
508
|
+
expect(yaml).to.include(`app: ${UNDERPOST_MONITORING.grafana.name}`);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
describe('grafana dashboards', () => {
|
|
513
|
+
it('reads Envoy 5xx from the labelled response-class family Envoy actually exposes', () => {
|
|
514
|
+
const dashboard = JSON.parse(envoyDashboardFactory());
|
|
515
|
+
const expressions = dashboard.panels.flatMap((panel) => panel.targets.map((target) => target.expr));
|
|
516
|
+
expect(expressions.some((expr) => expr.includes('envoy_response_code_class="5"'))).to.equal(true);
|
|
517
|
+
for (const metric of [
|
|
518
|
+
'envoy_http_downstream_rq_completed',
|
|
519
|
+
'envoy_http_downstream_cx_active',
|
|
520
|
+
'envoy_server_memory_allocated',
|
|
521
|
+
'envoy_server_uptime',
|
|
522
|
+
'envoy_http_downstream_cx_rx_bytes_total',
|
|
523
|
+
'envoy_http_downstream_cx_tx_bytes_total',
|
|
524
|
+
])
|
|
525
|
+
expect(
|
|
526
|
+
expressions.some((expr) => expr.includes(metric)),
|
|
527
|
+
metric,
|
|
528
|
+
).to.equal(true);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('produces dashboards Grafana can provision', () => {
|
|
532
|
+
for (const dashboard of [envoyDashboardFactory(), eventsDashboardFactory()]) {
|
|
533
|
+
const parsed = JSON.parse(dashboard);
|
|
534
|
+
expect(parsed.uid).to.be.a('string');
|
|
535
|
+
expect(parsed.panels.length).to.be.greaterThan(0);
|
|
536
|
+
for (const panel of parsed.panels) expect(panel.datasource.uid).to.equal('underpost-prometheus');
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
describe('eventsDashboardFactory', () => {
|
|
542
|
+
const dashboard = JSON.parse(eventsDashboardFactory());
|
|
543
|
+
const panel = (title) => dashboard.panels.find((entry) => entry.title === title);
|
|
544
|
+
const booleanPanels = dashboard.panels.filter((entry) =>
|
|
545
|
+
entry.targets.some((target) => /\bprobe_success|\bup\{/.test(target.expr)),
|
|
546
|
+
);
|
|
547
|
+
const rollupTitles = new Set([
|
|
548
|
+
'Probes failing now',
|
|
549
|
+
'Probe availability · range',
|
|
550
|
+
'Targets failing now',
|
|
551
|
+
'Target availability · range',
|
|
552
|
+
]);
|
|
553
|
+
const subjectPanels = booleanPanels.filter((entry) => !rollupTitles.has(entry.title));
|
|
554
|
+
|
|
555
|
+
it('renders no boolean availability series on a numeric axis', () => {
|
|
556
|
+
for (const entry of booleanPanels) expect(entry.type, entry.title).to.not.equal('timeseries');
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('tracks availability history as state timelines', () => {
|
|
560
|
+
for (const title of ['Probe success by event', 'Target availability'])
|
|
561
|
+
expect(panel(title).type, title).to.equal('state-timeline');
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('reduces each timeline step so a short outage survives a wide range', () => {
|
|
565
|
+
for (const title of ['Probe success by event', 'Target availability'])
|
|
566
|
+
for (const target of panel(title).targets)
|
|
567
|
+
expect(target.expr, title).to.match(/^min_over_time\(.+\[\$__rate_interval\]\)$/);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('maps 0 and 1 to words and colours on every availability panel', () => {
|
|
571
|
+
for (const entry of subjectPanels) {
|
|
572
|
+
const { mappings, thresholds } = entry.fieldConfig.defaults;
|
|
573
|
+
const [mapping] = mappings;
|
|
574
|
+
expect(mapping.options['0'], entry.title).to.include({ text: 'DOWN', color: 'red' });
|
|
575
|
+
expect(mapping.options['1'], entry.title).to.include({ text: 'UP', color: 'green' });
|
|
576
|
+
expect(thresholds.steps, entry.title).to.deep.equal([
|
|
577
|
+
{ color: 'red', value: null },
|
|
578
|
+
{ color: 'green', value: 1 },
|
|
579
|
+
]);
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('reads current state from instant queries and background-coloured cards', () => {
|
|
584
|
+
for (const title of ['Probe status by event', 'Target status']) {
|
|
585
|
+
const entry = panel(title);
|
|
586
|
+
expect(entry.type, title).to.equal('stat');
|
|
587
|
+
expect(entry.options.colorMode, title).to.equal('background');
|
|
588
|
+
expect(entry.options.textMode, title).to.equal('value_and_name');
|
|
589
|
+
for (const target of entry.targets) expect(target.instant, title).to.equal(true);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('states uptime as a fraction of the selected range against an SLO', () => {
|
|
594
|
+
for (const title of ['Probe availability · range', 'Target availability · range']) {
|
|
595
|
+
const entry = panel(title);
|
|
596
|
+
expect(entry.fieldConfig.defaults.unit, title).to.equal('percentunit');
|
|
597
|
+
expect(
|
|
598
|
+
entry.fieldConfig.defaults.thresholds.steps.map((step) => step.value),
|
|
599
|
+
title,
|
|
600
|
+
).to.deep.equal([null, 0.99, 0.999]);
|
|
601
|
+
for (const target of entry.targets) expect(target.expr, title).to.include('avg_over_time');
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('counts nothing down as a word rather than a zero', () => {
|
|
606
|
+
for (const title of ['Probes failing now', 'Targets failing now']) {
|
|
607
|
+
const entry = panel(title);
|
|
608
|
+
expect(entry.targets[0].expr, title).to.include('or vector(0)');
|
|
609
|
+
expect(entry.fieldConfig.defaults.mappings[0].options['0'], title).to.include({ text: 'ALL UP' });
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
it('narrows dozens of targets through dashboard variables', () => {
|
|
614
|
+
expect(dashboard.templating.list.map((variable) => variable.name)).to.deep.equal(['event', 'job']);
|
|
615
|
+
for (const variable of dashboard.templating.list) {
|
|
616
|
+
expect(variable.multi, variable.name).to.equal(true);
|
|
617
|
+
expect(variable.includeAll, variable.name).to.equal(true);
|
|
618
|
+
expect(variable.allValue, variable.name).to.equal('.+');
|
|
619
|
+
}
|
|
620
|
+
for (const entry of booleanPanels)
|
|
621
|
+
for (const target of entry.targets)
|
|
622
|
+
expect(target.expr, entry.title).to.match(/underpost_event=~"\$event"|job=~"\$job"/);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('labels every series by the subject an operator acts on', () => {
|
|
626
|
+
for (const entry of subjectPanels)
|
|
627
|
+
for (const target of entry.targets)
|
|
628
|
+
expect(target.legendFormat, entry.title).to.match(/\{\{(underpost_event|job)\}\} · \{\{instance\}\}/);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
describe('hubTunnelAddressFactory', () => {
|
|
633
|
+
it('reads the hub address straight from topology on the hub itself', () => {
|
|
634
|
+
expect(hubTunnelAddressFactory({ role: 'hub', address: '10.0.0.1/24' })).to.equal('10.0.0.1');
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it("derives the hub as the tunnel network's first host on a spoke", () => {
|
|
638
|
+
expect(hubTunnelAddressFactory({ role: 'control', address: '10.8.0.5/24' })).to.equal('10.8.0.1');
|
|
639
|
+
expect(hubTunnelAddressFactory({ role: 'worker', address: '10.0.0.7' })).to.equal('10.0.0.1');
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('yields nothing when the network holds no separate hub host', () => {
|
|
643
|
+
expect(hubTunnelAddressFactory({ role: 'control', address: '10.0.0.9/32' })).to.equal('');
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
describe('deployedEventIdsFactory', () => {
|
|
648
|
+
it('reads event ids back out of a rendered scrape config', () => {
|
|
649
|
+
expect(
|
|
650
|
+
deployedEventIdsFactory(" labels:\n underpost_event: 'public-ingress-down'\n"),
|
|
651
|
+
).to.deep.equal(['public-ingress-down']);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('reads them out of a rendered rule expression too', () => {
|
|
655
|
+
expect(deployedEventIdsFactory('expr: probe_success{underpost_event="wireguard-spoke-down"} == 0')).to.deep.equal([
|
|
656
|
+
'wireguard-spoke-down',
|
|
657
|
+
]);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('ignores the Alertmanager catch-all matcher', () => {
|
|
661
|
+
expect(deployedEventIdsFactory('- underpost_event =~ ".+"')).to.deep.equal([]);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('reports one distinct sorted set across documents', () => {
|
|
665
|
+
expect(
|
|
666
|
+
deployedEventIdsFactory("underpost_event: 'b-event'", "underpost_event: 'a-event'", "underpost_event: 'b-event'"),
|
|
667
|
+
).to.deep.equal(['a-event', 'b-event']);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it('reports nothing when the stack is absent', () => {
|
|
671
|
+
expect(deployedEventIdsFactory('', undefined)).to.deep.equal([]);
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
describe('event webhook host', () => {
|
|
676
|
+
it('resolves the control plane, not whichever node the API server lists first', () => {
|
|
677
|
+
const source = fs.readFileSync(new URL('../../../../src/cli/monitor.js', import.meta.url), 'utf8');
|
|
678
|
+
const body = source.slice(source.indexOf('nodeInternalIp(nodeName = '));
|
|
679
|
+
const method = body.slice(0, body.indexOf('\n },'));
|
|
680
|
+
expect(method).to.include('node-role.kubernetes.io/control-plane');
|
|
681
|
+
expect(method).to.not.match(/kubectl get nodes -o jsonpath/);
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
describe('envoy scrape hygiene', () => {
|
|
686
|
+
const conf = prometheusConfFactory({});
|
|
687
|
+
const job = conf.slice(conf.indexOf("job_name: 'envoy-gateway'"), conf.indexOf("job_name: 'underpost"));
|
|
688
|
+
|
|
689
|
+
it('drops the family Envoy emits with colliding labels', () => {
|
|
690
|
+
expect(job).to.include('metric_relabel_configs:');
|
|
691
|
+
expect(job).to.include('regex: envoy_cluster_total_match_count');
|
|
692
|
+
expect(job).to.include('action: drop');
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('keeps every series the dashboards actually read', () => {
|
|
696
|
+
for (const metric of [
|
|
697
|
+
'envoy_server_uptime',
|
|
698
|
+
'envoy_http_downstream_cx_active',
|
|
699
|
+
'envoy_http_downstream_rq_completed',
|
|
700
|
+
'envoy_http_downstream_rq_xx',
|
|
701
|
+
])
|
|
702
|
+
expect(job, metric).to.not.include(metric);
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
describe('nodeMetricsDashboardFactory', () => {
|
|
707
|
+
const dashboard = JSON.parse(nodeMetricsDashboardFactory());
|
|
708
|
+
|
|
709
|
+
it('is published under its own identity', () => {
|
|
710
|
+
expect(dashboard.uid).to.equal('underpost-node-metrics');
|
|
711
|
+
expect(dashboard.title).to.equal('Underpost · Node Metrics');
|
|
712
|
+
expect(dashboard.tags).to.deep.equal(['underpost', 'nodes', 'hardware']);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it('graphs cpu, memory, network, disk and the hub quota over time', () => {
|
|
716
|
+
expect(dashboard.panels.map((panel) => panel.title)).to.deep.equal([
|
|
717
|
+
'Node CPU Usage %',
|
|
718
|
+
'Node Memory Usage %',
|
|
719
|
+
'Network Throughput (RX/TX)',
|
|
720
|
+
'Disk Usage % & I/O Rates',
|
|
721
|
+
'Vultr Hub Monthly Bandwidth Usage',
|
|
722
|
+
]);
|
|
723
|
+
expect(dashboard.panels.every((panel) => panel.type === 'timeseries')).to.equal(true);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it('reads the hub quota from the scraped metric, never from the Vultr API', () => {
|
|
727
|
+
const quota = dashboard.panels.at(-1);
|
|
728
|
+
expect(quota.targets[0].expr).to.equal('(vultr_bandwidth_used_bytes / vultr_bandwidth_limit_bytes) * 100');
|
|
729
|
+
expect(quota.targets[0].legendFormat).to.equal('Hub Bandwidth Usage % (hub)');
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it('never plots a byte rate under a percent axis', () => {
|
|
733
|
+
for (const panel of dashboard.panels.filter((panel) => panel.fieldConfig.defaults.unit === 'percent')) {
|
|
734
|
+
const byteRates = panel.targets.filter((target) => /rate\(\w*_bytes_total/.test(target.expr));
|
|
735
|
+
const reunited = new Set(
|
|
736
|
+
panel.fieldConfig.overrides
|
|
737
|
+
.filter((override) => override.properties.some((property) => property.id === 'unit'))
|
|
738
|
+
.map((override) => override.matcher.options),
|
|
739
|
+
);
|
|
740
|
+
for (const rate of byteRates) expect(reunited.has(rate.refId), `${panel.title} ${rate.refId}`).to.equal(true);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it('moves a re-united series onto its own axis', () => {
|
|
745
|
+
const disk = dashboard.panels.find((panel) => panel.title === 'Disk Usage % & I/O Rates');
|
|
746
|
+
expect(disk.fieldConfig.overrides).to.have.lengthOf(2);
|
|
747
|
+
for (const override of disk.fieldConfig.overrides)
|
|
748
|
+
expect(override.properties).to.deep.equal([
|
|
749
|
+
{ id: 'unit', value: 'binBps' },
|
|
750
|
+
{ id: 'custom.axisPlacement', value: 'right' },
|
|
751
|
+
]);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it('names the role beside the host in every per-node legend', () => {
|
|
755
|
+
const perNode = dashboard.panels.filter((panel) => panel.targets.some((target) => target.expr.includes('node_')));
|
|
756
|
+
expect(perNode).to.have.lengthOf(4);
|
|
757
|
+
for (const panel of perNode)
|
|
758
|
+
for (const target of panel.targets)
|
|
759
|
+
expect(target.legendFormat, panel.title).to.include('{{instance}} ({{underpost_role}})');
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('measures whatever a host calls its interface, the hub included', () => {
|
|
763
|
+
const network = dashboard.panels.find((panel) => panel.title === 'Network Throughput (RX/TX)');
|
|
764
|
+
for (const target of network.targets) {
|
|
765
|
+
expect(target.expr).to.include('device!~');
|
|
766
|
+
expect(target.expr).to.not.include('enp.*');
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
describe('nodeExporterManifestFactory', () => {
|
|
772
|
+
const manifest = nodeExporterManifestFactory({ namespace: 'default' });
|
|
773
|
+
|
|
774
|
+
it('measures the host, not the pod', () => {
|
|
775
|
+
expect(manifest).to.include('kind: DaemonSet');
|
|
776
|
+
expect(manifest).to.include('hostNetwork: true');
|
|
777
|
+
expect(manifest).to.include('hostPID: true');
|
|
778
|
+
expect(manifest).to.include('--path.rootfs=/host/root');
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it('never writes to what it measures', () => {
|
|
782
|
+
const mounts = manifest.slice(manifest.indexOf('volumeMounts:'), manifest.indexOf('volumes:'));
|
|
783
|
+
expect(mounts.match(/readOnly: true/g) || []).to.have.lengthOf(4);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
it('reads the textfile directory the bandwidth guard writes', () => {
|
|
787
|
+
expect(manifest).to.include(
|
|
788
|
+
`--collector.textfile.directory=${UNDERPOST_MONITORING.nodeExporter.textfileDirectory}`,
|
|
789
|
+
);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it('runs on every node, control plane included', () => {
|
|
793
|
+
expect(manifest).to.include('tolerations:');
|
|
794
|
+
expect(manifest).to.include('operator: Exists');
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
describe('host metric scrape jobs', () => {
|
|
799
|
+
it('discovers cluster nodes at the collector port', () => {
|
|
800
|
+
const conf = prometheusConfFactory({});
|
|
801
|
+
expect(conf).to.include("job_name: 'node-exporter'");
|
|
802
|
+
expect(conf).to.include('role: node');
|
|
803
|
+
expect(conf).to.include(`replacement: '$1:${UNDERPOST_MONITORING.nodeExporter.port}'`);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it('scrapes the hub statically, because discovery cannot see a VPS', () => {
|
|
807
|
+
const conf = prometheusConfFactory({ hostTargets: ['10.0.0.1'] });
|
|
808
|
+
expect(conf).to.include("job_name: 'node-exporter-hub'");
|
|
809
|
+
expect(conf).to.include("targets: ['10.0.0.1:9100']");
|
|
810
|
+
expect(conf).to.include("underpost_role: 'hub'");
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it('emits no hub job when no hub is registered', () => {
|
|
814
|
+
expect(prometheusConfFactory({})).to.not.include('node-exporter-hub');
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it('relabels the role a discovered node carries only in the deploy registry', () => {
|
|
818
|
+
const conf = prometheusConfFactory({
|
|
819
|
+
nodeRoles: [
|
|
820
|
+
{ nodeName: 'localhost.localdomain', role: 'control' },
|
|
821
|
+
{ nodeName: 'worker-a', role: 'worker' },
|
|
822
|
+
],
|
|
823
|
+
});
|
|
824
|
+
expect(conf).to.include("regex: 'localhost\\.localdomain'");
|
|
825
|
+
expect(conf).to.include("replacement: 'control'");
|
|
826
|
+
expect(conf).to.include("regex: 'worker-a'");
|
|
827
|
+
expect(conf).to.include("replacement: 'worker'");
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it('leaves a node with no document labelled, so no series loses its role', () => {
|
|
831
|
+
expect(prometheusConfFactory({})).to.include("replacement: 'cluster'");
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
describe('nodeExporterServiceFactory', () => {
|
|
836
|
+
const { nodeExporter } = UNDERPOST_MONITORING;
|
|
837
|
+
const unit = nodeExporterServiceFactory({ host: '10.0.0.1' });
|
|
838
|
+
|
|
839
|
+
it('publishes the collector on the tunnel only, never on the public address', () => {
|
|
840
|
+
expect(unit).to.include(`--web.listen-address=10.0.0.1:${nodeExporter.port}`);
|
|
841
|
+
expect(unit).to.include('Requires=wg-quick@wg0.service');
|
|
842
|
+
expect(unit).to.include('PartOf=wg-quick@wg0.service');
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('collects what the cluster DaemonSet collects', () => {
|
|
846
|
+
expect(unit).to.include(`--collector.textfile.directory=${nodeExporter.textfileDirectory}`);
|
|
847
|
+
expect(unit).to.include(`--collector.filesystem.mount-points-exclude=${nodeExporter.filesystemExclude}`);
|
|
848
|
+
expect(nodeExporterManifestFactory({})).to.include(
|
|
849
|
+
`--collector.filesystem.mount-points-exclude=${nodeExporter.filesystemExclude}`,
|
|
850
|
+
);
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
describe('nodeExporterServiceScriptFactory', () => {
|
|
855
|
+
const command = nodeExporterServiceScriptFactory({ host: '10.0.0.1' });
|
|
856
|
+
const script = Buffer.from(command.split(' ')[1], 'base64').toString('utf8');
|
|
857
|
+
|
|
858
|
+
it('survives the remote runner, which interpolates into a double-quoted bash -lc', () => {
|
|
859
|
+
expect(command).to.not.include('"');
|
|
860
|
+
expect(command).to.not.include('$');
|
|
861
|
+
expect(command.split('\n')).to.have.lengthOf(1);
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
it('installs the pinned collector only when the host does not already run it', () => {
|
|
865
|
+
expect(script).to.include(`VERSION=${UNDERPOST_MONITORING.nodeExporter.version}`);
|
|
866
|
+
expect(script).to.include('grep -q "version $VERSION"');
|
|
867
|
+
expect(script).to.include('releases/download/v$VERSION/$RELEASE.tar.gz');
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it('resolves the release for the architecture it lands on', () => {
|
|
871
|
+
expect(script).to.include('ARCH=$(uname -m)');
|
|
872
|
+
expect(script).to.include('x86_64) ARCH=amd64');
|
|
873
|
+
expect(script).to.include('aarch64) ARCH=arm64');
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
it('fails the run when the service did not come up', () => {
|
|
877
|
+
expect(script.trim().split('\n').at(-1)).to.equal(
|
|
878
|
+
`systemctl is-active --quiet ${UNDERPOST_MONITORING.nodeExporter.serviceName}`,
|
|
879
|
+
);
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
describe('cron job bandwidth publication', () => {
|
|
884
|
+
const manifest = cronJobYamlFactory({
|
|
885
|
+
name: 'dd-cron-vultr',
|
|
886
|
+
expression: '*/30 * * * *',
|
|
887
|
+
deployList: 'dd-cron',
|
|
888
|
+
jobList: 'vultr',
|
|
889
|
+
kubeadm: true,
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
it('mounts the host textfile directory the collector reads', () => {
|
|
893
|
+
const { textfileDirectory } = UNDERPOST_MONITORING.nodeExporter;
|
|
894
|
+
expect(manifest).to.include(`mountPath: ${textfileDirectory}`);
|
|
895
|
+
expect(manifest).to.include(`path: ${textfileDirectory}`);
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
it('creates the directory, so a node that never ran the collector still publishes', () => {
|
|
899
|
+
const volume = manifest.slice(manifest.indexOf(UNDERPOST_MONITORING.nodeExporter.textfileDirectory));
|
|
900
|
+
expect(volume).to.include('type: DirectoryOrCreate');
|
|
901
|
+
});
|
|
902
|
+
});
|