@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,1724 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cluster observability stack: Prometheus, Alertmanager, the Blackbox
|
|
3
|
+
* Exporter and Grafana.
|
|
4
|
+
*
|
|
5
|
+
* Every file these four read is derived, never hand-written: scrape targets come
|
|
6
|
+
* from the same `conf.server.json` the runtime binds, probe targets and alert
|
|
7
|
+
* rules come from the event registry that also holds the remediation handlers,
|
|
8
|
+
* and the Alertmanager route points back at the dispatcher that owns them. A
|
|
9
|
+
* probe with no handler and a handler with no probe are therefore both
|
|
10
|
+
* unrepresentable.
|
|
11
|
+
*
|
|
12
|
+
* This module is pure: it renders configuration and manifests from values it is
|
|
13
|
+
* given. Reading deploy configuration, talking to the cluster and reloading the
|
|
14
|
+
* running components belong to `src/cli/monitor.js`.
|
|
15
|
+
*
|
|
16
|
+
* @module src/server/ops/monitoring.js
|
|
17
|
+
* @namespace UnderpostMonitoring
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { containerStorageCommandsFactory } from '../security/container-storage.js';
|
|
21
|
+
import { systemdServiceCommandsFactory, systemdUnitFactory } from './systemd.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @constant UNDERPOST_MONITORING
|
|
25
|
+
* @description Identity of the observability workloads. One stack serves every
|
|
26
|
+
* deploy on the cluster, so these names are cluster-wide constants rather than
|
|
27
|
+
* per-deploy.
|
|
28
|
+
* @memberof UnderpostMonitoring
|
|
29
|
+
*/
|
|
30
|
+
const UNDERPOST_MONITORING = {
|
|
31
|
+
scrapeInterval: '30s',
|
|
32
|
+
prometheus: {
|
|
33
|
+
name: 'prometheus',
|
|
34
|
+
configMapName: 'prometheus-config',
|
|
35
|
+
rulesConfigMapName: 'prometheus-rules',
|
|
36
|
+
port: 9090,
|
|
37
|
+
},
|
|
38
|
+
alertmanager: {
|
|
39
|
+
name: 'alertmanager',
|
|
40
|
+
configMapName: 'alertmanager-config',
|
|
41
|
+
secretName: 'alertmanager-webhook',
|
|
42
|
+
tokenKey: 'token',
|
|
43
|
+
port: 9093,
|
|
44
|
+
},
|
|
45
|
+
blackbox: {
|
|
46
|
+
name: 'blackbox-exporter',
|
|
47
|
+
configMapName: 'blackbox-exporter-config',
|
|
48
|
+
port: 9115,
|
|
49
|
+
},
|
|
50
|
+
grafana: {
|
|
51
|
+
name: 'grafana',
|
|
52
|
+
adminSecretName: 'grafana-admin',
|
|
53
|
+
adminUserKey: 'admin-user',
|
|
54
|
+
adminPasswordKey: 'admin-password',
|
|
55
|
+
adminSecretVersionAnnotation: 'underpost.net/grafana-admin-secret-version',
|
|
56
|
+
adminLoginAnnotation: 'underpost.net/grafana-admin-login',
|
|
57
|
+
pvcName: 'grafana-pvc',
|
|
58
|
+
nodePortName: 'grafana-nodeport',
|
|
59
|
+
routeName: 'grafana-route',
|
|
60
|
+
datasourcesConfigMapName: 'grafana-datasources',
|
|
61
|
+
dashboardProviderConfigMapName: 'grafana-dashboard-provider',
|
|
62
|
+
dashboardsConfigMapName: 'grafana-dashboards',
|
|
63
|
+
port: 3000,
|
|
64
|
+
nodePort: 32300,
|
|
65
|
+
// Sub-path under an existing hostname, so the dashboards ride the edge
|
|
66
|
+
// certificate already issued for that host instead of needing one of their own.
|
|
67
|
+
subPath: '/grafana',
|
|
68
|
+
dashboardsPath: '/var/lib/grafana/dashboards',
|
|
69
|
+
},
|
|
70
|
+
// Envoy Gateway publishes the data plane's admin metrics on this port; the
|
|
71
|
+
// path is Envoy's own, not a Kubernetes convention.
|
|
72
|
+
envoy: {
|
|
73
|
+
namespace: 'envoy-gateway-system',
|
|
74
|
+
metricsPort: 19001,
|
|
75
|
+
metricsPath: '/stats/prometheus',
|
|
76
|
+
},
|
|
77
|
+
// Host metrics. `hostPort` on a DaemonSet rather than a Service: Prometheus
|
|
78
|
+
// discovers nodes, not endpoints, so the address it scrapes is the node's own
|
|
79
|
+
// and the series carry the node identity the alerts act on.
|
|
80
|
+
nodeExporter: {
|
|
81
|
+
name: 'node-exporter',
|
|
82
|
+
port: 9100,
|
|
83
|
+
// Read by the collector and written by anything that has a number the
|
|
84
|
+
// cluster cannot scrape for itself, the Vultr quota above all.
|
|
85
|
+
textfileDirectory: '/var/lib/node_exporter/textfile',
|
|
86
|
+
// The hub is a VPS, not a cluster node, so it runs the same collector as a
|
|
87
|
+
// systemd service instead of as a DaemonSet pod.
|
|
88
|
+
version: '1.9.1',
|
|
89
|
+
binaryPath: '/usr/local/bin/node_exporter',
|
|
90
|
+
serviceName: 'underpost-node-exporter.service',
|
|
91
|
+
unitPath: '/etc/systemd/system/underpost-node-exporter.service',
|
|
92
|
+
filesystemExclude: '^/(dev|proc|sys|var/lib/(docker|containers|kubelet)|run)($|/)',
|
|
93
|
+
// Physical and tunnel interfaces, named by what they are not: enumerating
|
|
94
|
+
// them misses whatever a given host calls its NIC (ens3 on the hub).
|
|
95
|
+
networkDeviceSelector: 'device!~"lo|veth.*|docker.*|br-.*|cni.*|flannel.*|cali.*|tunl.*|virbr.*"',
|
|
96
|
+
},
|
|
97
|
+
// Where Alertmanager delivers, and where `underpost event --serve` listens.
|
|
98
|
+
eventWebhook: {
|
|
99
|
+
port: 39099,
|
|
100
|
+
path: '/event',
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @method serviceDnsFactory
|
|
106
|
+
* @description Fully qualified in-cluster address of a stack component.
|
|
107
|
+
*
|
|
108
|
+
* Always qualified: the stack is namespaced by `--namespace`, and a short name
|
|
109
|
+
* only resolves for a client that happens to share that namespace. Prometheus
|
|
110
|
+
* scraping the Blackbox Exporter is such a client only by coincidence.
|
|
111
|
+
* @param {string} name - Service name.
|
|
112
|
+
* @param {number} port - Service port.
|
|
113
|
+
* @param {string} [namespace] - Namespace holding the Service.
|
|
114
|
+
* @returns {string} `<name>.<namespace>.svc.cluster.local:<port>`.
|
|
115
|
+
* @memberof UnderpostMonitoring
|
|
116
|
+
*/
|
|
117
|
+
const serviceDnsFactory = (name, port, namespace = 'default') => `${name}.${namespace}.svc.cluster.local:${port}`;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @method metricsPathFactory
|
|
121
|
+
* @description The `/metrics` route an Express runtime mounts for a proxy
|
|
122
|
+
* sub-path. Shared with the runtime itself so a scrape target can never point
|
|
123
|
+
* at a route the server does not serve.
|
|
124
|
+
* @param {string} [path] - Proxy sub-path (`/`, `/api`).
|
|
125
|
+
* @returns {string} Metrics route.
|
|
126
|
+
* @memberof UnderpostMonitoring
|
|
127
|
+
*/
|
|
128
|
+
const metricsPathFactory = (path = '/') => `${path === '/' ? '' : path}/metrics`;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @method scrapeDeployListFactory
|
|
132
|
+
* @description Selects the deploys the stack scrapes.
|
|
133
|
+
*
|
|
134
|
+
* The default set is the cron deploy plus the router deploys, which is exactly
|
|
135
|
+
* what `loadCronDeployEnv()` loads. The router alone would leave the cron deploy
|
|
136
|
+
* — the one that runs the backups, the DNS records and the bandwidth guard — as
|
|
137
|
+
* the single unmonitored runtime on the cluster. An explicit id or list is taken
|
|
138
|
+
* as given, so narrowing to one deploy stays possible.
|
|
139
|
+
* @param {object} params
|
|
140
|
+
* @param {string} [params.deployId] - Explicit deploy id or comma-separated list; `dd` or empty selects the default set.
|
|
141
|
+
* @param {string} [params.cronDeployId] - Deploy id from `engine-private/deploy/dd.cron`.
|
|
142
|
+
* @param {string[]} [params.routerDeployIds] - Deploy ids from `engine-private/deploy/dd.routes`.
|
|
143
|
+
* @returns {string[]} Distinct deploy ids, in resolution order.
|
|
144
|
+
* @memberof UnderpostMonitoring
|
|
145
|
+
*/
|
|
146
|
+
const scrapeDeployListFactory = ({ deployId = '', cronDeployId = '', routerDeployIds = [] } = {}) => {
|
|
147
|
+
const explicit = `${deployId || ''}`
|
|
148
|
+
.split(',')
|
|
149
|
+
.map((id) => id.trim())
|
|
150
|
+
.filter(Boolean);
|
|
151
|
+
if (explicit.length > 0 && !explicit.includes('dd')) return [...new Set(explicit)];
|
|
152
|
+
return [...new Set([`${cronDeployId || ''}`.trim(), ...routerDeployIds.map((id) => `${id}`.trim())].filter(Boolean))];
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @method appScrapeEntriesFactory
|
|
157
|
+
* @description Scrape entries for the Express runtimes a deploy declares.
|
|
158
|
+
*
|
|
159
|
+
* Only `nodejs` paths are emitted: they are the ones `ExpressService.createApp`
|
|
160
|
+
* gives a `prom-client` registry to. Redirect-only and `lampp`/`wp` paths serve
|
|
161
|
+
* no registry, and scraping them would produce permanently-down targets that
|
|
162
|
+
* make the stack's own health unreadable.
|
|
163
|
+
* @param {object} confServer - Parsed `conf.server.json` (host → path → config).
|
|
164
|
+
* @param {object} [params]
|
|
165
|
+
* @param {string} [params.scheme] - `https` in production, `http` in development.
|
|
166
|
+
* @returns {Array<{host: string, metricsPath: string, scheme: string}>} Scrape entries.
|
|
167
|
+
* @memberof UnderpostMonitoring
|
|
168
|
+
*/
|
|
169
|
+
const appScrapeEntriesFactory = (confServer = {}, { scheme = 'https' } = {}) => {
|
|
170
|
+
const entries = [];
|
|
171
|
+
for (const host of Object.keys(confServer)) {
|
|
172
|
+
for (const path of Object.keys(confServer[host] || {})) {
|
|
173
|
+
const conf = confServer[host][path] || {};
|
|
174
|
+
if (conf.runtime !== 'nodejs' || conf.redirect) continue;
|
|
175
|
+
entries.push({ host, metricsPath: metricsPathFactory(path), scheme });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return entries;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const yamlList = (values = []) => `[${values.map((value) => `'${value}'`).join(', ')}]`;
|
|
182
|
+
|
|
183
|
+
/** Escapes a literal for a fully anchored Prometheus relabel regex. */
|
|
184
|
+
const promRegexEscape = (value = '') => `${value}`.replace(/[\\^$.|?*+()[\]{}]/g, '\\$&');
|
|
185
|
+
|
|
186
|
+
const indentBlock = (text, spaces) =>
|
|
187
|
+
`${text}`
|
|
188
|
+
.replace(/\n$/, '')
|
|
189
|
+
.split('\n')
|
|
190
|
+
.map((line) => (line.length > 0 ? `${' '.repeat(spaces)}${line}` : ''))
|
|
191
|
+
.join('\n');
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @method probeGroupsFactory
|
|
195
|
+
* @description Groups event probes by Blackbox module and probe interval, so one
|
|
196
|
+
* scrape job serves every event that probes the same way, at the same cadence.
|
|
197
|
+
*
|
|
198
|
+
* Each probe keeps its own label set as a distinct `static_configs` group. That
|
|
199
|
+
* is what lets one event fan out over many subjects — a probe per WireGuard
|
|
200
|
+
* spoke, each labelled with its id — so the alert Prometheus raises names the
|
|
201
|
+
* subject that failed instead of the event that covers them all.
|
|
202
|
+
*
|
|
203
|
+
* The interval is the event's own `probeInterval`, so a cheap tunnel ping and a
|
|
204
|
+
* fan-out over every public host do not have to share a cadence. Prometheus is
|
|
205
|
+
* the scheduler; this only tells it what each job's period is.
|
|
206
|
+
* @param {Array<{eventId: string, module: string, interval: string, targets: string[], labels: object}>} [probes] - Registered probes.
|
|
207
|
+
* @returns {Array<{module: string, interval: string, groups: Array<{targets: string[], labels: object}>}>} Grouped probes.
|
|
208
|
+
* @memberof UnderpostMonitoring
|
|
209
|
+
*/
|
|
210
|
+
const probeGroupsFactory = (probes = []) => {
|
|
211
|
+
const byJob = new Map();
|
|
212
|
+
for (const probe of probes) {
|
|
213
|
+
const targets = (probe.targets || []).filter(Boolean);
|
|
214
|
+
if (!probe.module || !probe.eventId || targets.length === 0) continue;
|
|
215
|
+
const interval = `${probe.interval || ''}`.trim();
|
|
216
|
+
const key = `${probe.module}|${interval}`;
|
|
217
|
+
if (!byJob.has(key)) byJob.set(key, { module: probe.module, interval, groups: [] });
|
|
218
|
+
byJob.get(key).groups.push({ targets, labels: { underpost_event: probe.eventId, ...(probe.labels || {}) } });
|
|
219
|
+
}
|
|
220
|
+
return [...byJob.values()];
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @method deployedEventIdsFactory
|
|
225
|
+
* @description The event ids a rendered monitoring configuration carries.
|
|
226
|
+
*
|
|
227
|
+
* Every probe group and every rule is labelled `underpost_event`, so the
|
|
228
|
+
* generated documents already state which events they serve. Reading them back
|
|
229
|
+
* makes the cluster its own record of what is deployed — no local state file to
|
|
230
|
+
* fall out of step with the objects that actually exist.
|
|
231
|
+
* @param {...string} documents - Rendered ConfigMap data (`prometheus.yml`, rules).
|
|
232
|
+
* @returns {string[]} Distinct event ids, sorted.
|
|
233
|
+
* @memberof UnderpostMonitoring
|
|
234
|
+
*/
|
|
235
|
+
const deployedEventIdsFactory = (...documents) =>
|
|
236
|
+
[
|
|
237
|
+
...new Set(
|
|
238
|
+
documents.flatMap((document) =>
|
|
239
|
+
[...`${document || ''}`.matchAll(/underpost_event\s*[:=]~?\s*'?"?([a-z0-9-]+)'?"?/gi)].map((match) => match[1]),
|
|
240
|
+
),
|
|
241
|
+
),
|
|
242
|
+
]
|
|
243
|
+
.filter((id) => id && id !== '.+')
|
|
244
|
+
.sort();
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @method prometheusConfFactory
|
|
248
|
+
* @description Renders `prometheus.yml`.
|
|
249
|
+
*
|
|
250
|
+
* The Envoy job discovers the data plane instead of naming it: Envoy Gateway
|
|
251
|
+
* provisions its own Deployment per GatewayClass with a generated name, so a
|
|
252
|
+
* static target would break on every GatewayClass change. Pod discovery needs
|
|
253
|
+
* the RBAC the Prometheus manifest carries.
|
|
254
|
+
* @param {object} params
|
|
255
|
+
* @param {Array<{host: string, metricsPath: string, scheme: string}>} [params.appTargets] - Express runtime targets.
|
|
256
|
+
* @param {string[]} [params.extraTargets] - Additional `host:port` targets scraped over HTTP at `/metrics`.
|
|
257
|
+
* @param {Array<{eventId: string, module: string, targets: string[]}>} [params.probes] - Blackbox probes.
|
|
258
|
+
* @param {Array<{nodeName: string, role: string}>} [params.nodeRoles] - Registered cluster
|
|
259
|
+
* nodes, relabelled onto their discovered series as `underpost_role`.
|
|
260
|
+
* @param {string} [params.namespace] - Namespace holding the stack.
|
|
261
|
+
* @param {string} [params.scrapeInterval] - Global scrape/evaluation interval.
|
|
262
|
+
* @returns {string} `prometheus.yml` contents.
|
|
263
|
+
* @memberof UnderpostMonitoring
|
|
264
|
+
*/
|
|
265
|
+
const prometheusConfFactory = ({
|
|
266
|
+
appTargets = [],
|
|
267
|
+
extraTargets = [],
|
|
268
|
+
probes = [],
|
|
269
|
+
hostTargets = [],
|
|
270
|
+
nodeRoles = [],
|
|
271
|
+
namespace = 'default',
|
|
272
|
+
scrapeInterval = UNDERPOST_MONITORING.scrapeInterval,
|
|
273
|
+
} = {}) => {
|
|
274
|
+
const { prometheus, alertmanager, blackbox, envoy, nodeExporter } = UNDERPOST_MONITORING;
|
|
275
|
+
|
|
276
|
+
const appGroups = new Map();
|
|
277
|
+
for (const target of appTargets) {
|
|
278
|
+
const key = `${target.scheme}|${target.metricsPath}`;
|
|
279
|
+
if (!appGroups.has(key)) appGroups.set(key, []);
|
|
280
|
+
appGroups.get(key).push(target.host);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const appJobs = [...appGroups.entries()].map(([key, hosts], index) => {
|
|
284
|
+
const [scheme, metricsPath] = key.split('|');
|
|
285
|
+
return `
|
|
286
|
+
- job_name: 'underpost-express${index > 0 ? `-${index}` : ''}'
|
|
287
|
+
metrics_path: ${metricsPath}
|
|
288
|
+
scheme: ${scheme}
|
|
289
|
+
tls_config:
|
|
290
|
+
insecure_skip_verify: true
|
|
291
|
+
static_configs:
|
|
292
|
+
- targets: ${yamlList([...new Set(hosts)])}`;
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const extraJob =
|
|
296
|
+
extraTargets.length > 0
|
|
297
|
+
? `
|
|
298
|
+
- job_name: 'underpost-extra'
|
|
299
|
+
metrics_path: /metrics
|
|
300
|
+
static_configs:
|
|
301
|
+
- targets: ${yamlList([...new Set(extraTargets)])}`
|
|
302
|
+
: '';
|
|
303
|
+
|
|
304
|
+
// Cluster nodes are discovered; the hub is a VPS that discovery cannot see, so
|
|
305
|
+
// it is scraped across the tunnel at the address the WireGuard events probe.
|
|
306
|
+
// Discovery knows a node's name, not the role the deploy registry gives it, so
|
|
307
|
+
// the role every panel and alert groups by is relabelled on from that registry.
|
|
308
|
+
const roleRelabels = nodeRoles
|
|
309
|
+
.filter((entry) => entry?.nodeName && entry?.role)
|
|
310
|
+
.map(
|
|
311
|
+
(entry) => `
|
|
312
|
+
- source_labels: [__meta_kubernetes_node_name]
|
|
313
|
+
regex: '${promRegexEscape(entry.nodeName)}'
|
|
314
|
+
target_label: underpost_role
|
|
315
|
+
replacement: '${entry.role}'`,
|
|
316
|
+
)
|
|
317
|
+
.join('');
|
|
318
|
+
|
|
319
|
+
const hostJobs = [
|
|
320
|
+
`
|
|
321
|
+
- job_name: '${nodeExporter.name}'
|
|
322
|
+
kubernetes_sd_configs:
|
|
323
|
+
- role: node
|
|
324
|
+
relabel_configs:
|
|
325
|
+
- source_labels: [__meta_kubernetes_node_address_InternalIP]
|
|
326
|
+
target_label: __address__
|
|
327
|
+
replacement: '\$1:${nodeExporter.port}'
|
|
328
|
+
- source_labels: [__meta_kubernetes_node_name]
|
|
329
|
+
target_label: node
|
|
330
|
+
- target_label: underpost_role
|
|
331
|
+
replacement: 'cluster'${roleRelabels}`,
|
|
332
|
+
...(hostTargets.length > 0
|
|
333
|
+
? [
|
|
334
|
+
`
|
|
335
|
+
- job_name: '${nodeExporter.name}-hub'
|
|
336
|
+
static_configs:
|
|
337
|
+
- targets: ${yamlList(hostTargets.map((target) => `${target}:${nodeExporter.port}`))}
|
|
338
|
+
labels:
|
|
339
|
+
underpost_role: 'hub'`,
|
|
340
|
+
]
|
|
341
|
+
: []),
|
|
342
|
+
];
|
|
343
|
+
|
|
344
|
+
const probeJobs = probeGroupsFactory(probes).map(
|
|
345
|
+
({ module, interval, groups }, index) => `
|
|
346
|
+
- job_name: 'blackbox-${module}${index > 0 ? `-${index}` : ''}'
|
|
347
|
+
metrics_path: /probe${interval ? `\n scrape_interval: ${interval}` : ''}
|
|
348
|
+
params:
|
|
349
|
+
module: ['${module}']
|
|
350
|
+
static_configs:
|
|
351
|
+
${groups
|
|
352
|
+
.map(
|
|
353
|
+
(group) => ` - targets: ${yamlList(group.targets)}
|
|
354
|
+
labels:
|
|
355
|
+
${Object.entries(group.labels)
|
|
356
|
+
.map(([key, value]) => ` ${key}: '${value}'`)
|
|
357
|
+
.join('\n')}`,
|
|
358
|
+
)
|
|
359
|
+
.join('\n')}
|
|
360
|
+
relabel_configs:
|
|
361
|
+
- source_labels: [__address__]
|
|
362
|
+
target_label: __param_target
|
|
363
|
+
- source_labels: [__param_target]
|
|
364
|
+
target_label: instance
|
|
365
|
+
- target_label: __address__
|
|
366
|
+
replacement: '${serviceDnsFactory(blackbox.name, blackbox.port, namespace)}'`,
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
return `global:
|
|
370
|
+
scrape_interval: ${scrapeInterval}
|
|
371
|
+
evaluation_interval: ${scrapeInterval}
|
|
372
|
+
|
|
373
|
+
rule_files:
|
|
374
|
+
- /etc/prometheus/rules/*.yml
|
|
375
|
+
|
|
376
|
+
alerting:
|
|
377
|
+
alertmanagers:
|
|
378
|
+
- static_configs:
|
|
379
|
+
- targets: ['${serviceDnsFactory(alertmanager.name, alertmanager.port, namespace)}']
|
|
380
|
+
|
|
381
|
+
scrape_configs:
|
|
382
|
+
- job_name: '${prometheus.name}'
|
|
383
|
+
static_configs:
|
|
384
|
+
- targets: ['127.0.0.1:${prometheus.port}']
|
|
385
|
+
|
|
386
|
+
- job_name: 'envoy-gateway'
|
|
387
|
+
metrics_path: ${envoy.metricsPath}
|
|
388
|
+
kubernetes_sd_configs:
|
|
389
|
+
- role: pod
|
|
390
|
+
namespaces:
|
|
391
|
+
names: ['${envoy.namespace}']
|
|
392
|
+
relabel_configs:
|
|
393
|
+
- source_labels: [__meta_kubernetes_pod_label_app_kubernetes_io_name]
|
|
394
|
+
regex: envoy
|
|
395
|
+
action: keep
|
|
396
|
+
- source_labels: [__meta_kubernetes_pod_ip]
|
|
397
|
+
target_label: __address__
|
|
398
|
+
replacement: '\$1:${envoy.metricsPort}'
|
|
399
|
+
- source_labels: [__meta_kubernetes_pod_name]
|
|
400
|
+
target_label: pod
|
|
401
|
+
- source_labels: [__meta_kubernetes_namespace]
|
|
402
|
+
target_label: namespace
|
|
403
|
+
metric_relabel_configs:
|
|
404
|
+
# Envoy truncates a cluster name at its first hyphen when it extracts the
|
|
405
|
+
# tag, so every www-* HTTPRoute reports as httproute/default/www. The
|
|
406
|
+
# samples arrive already indistinguishable, no relabeling can separate
|
|
407
|
+
# them, and Prometheus drops the scrape's worth of them as duplicates with
|
|
408
|
+
# the same timestamp. Nothing reads this socket-match counter, so it goes.
|
|
409
|
+
- source_labels: [__name__]
|
|
410
|
+
regex: envoy_cluster_total_match_count
|
|
411
|
+
action: drop
|
|
412
|
+
${[...appJobs, extraJob, ...hostJobs, ...probeJobs].filter(Boolean).join('\n')}
|
|
413
|
+
`;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* @method alertRulesFactory
|
|
418
|
+
* @description Renders the alerting rules for the registered events.
|
|
419
|
+
*
|
|
420
|
+
* Every rule carries `underpost_event`, because that label is the whole
|
|
421
|
+
* contract between the rule, the Alertmanager route and the dispatcher: the
|
|
422
|
+
* route matches on its presence and the dispatcher reads it to select a
|
|
423
|
+
* handler. A rule without it fires into the null receiver.
|
|
424
|
+
* @param {Array<object>} [events] - Registered events with an `alert` definition carrying its declared `for`.
|
|
425
|
+
* @returns {string} Rules file contents.
|
|
426
|
+
* @memberof UnderpostMonitoring
|
|
427
|
+
*/
|
|
428
|
+
const alertRulesFactory = (events = []) => {
|
|
429
|
+
const rules = events
|
|
430
|
+
// A rule needs its window as much as its expression: the contract declares
|
|
431
|
+
// `alertFor`, and rendering a default here would put the answer in two places.
|
|
432
|
+
.filter((event) => event.id && event.alert?.expr && event.alert?.for)
|
|
433
|
+
.map(
|
|
434
|
+
(event) => ` - alert: ${event.alert.name}
|
|
435
|
+
expr: ${event.alert.expr}
|
|
436
|
+
for: ${event.alert.for}
|
|
437
|
+
labels:
|
|
438
|
+
severity: ${event.alert.severity || 'critical'}
|
|
439
|
+
underpost_event: '${event.id}'
|
|
440
|
+
annotations:
|
|
441
|
+
summary: '${event.alert.summary || event.description || event.id}'
|
|
442
|
+
description: '${event.alert.description || event.description || event.id}'`,
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
return `groups:
|
|
446
|
+
- name: underpost-events
|
|
447
|
+
rules:
|
|
448
|
+
${rules.length > 0 ? rules.join('\n') : ' []'}
|
|
449
|
+
`;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* @method alertmanagerConfFactory
|
|
454
|
+
* @description Renders `alertmanager.yml`.
|
|
455
|
+
*
|
|
456
|
+
* One route matters: anything labelled `underpost_event` goes to the webhook
|
|
457
|
+
* the dispatcher serves. Everything else lands in a null receiver rather than
|
|
458
|
+
* an inbox, because an alert with no registered handler has no remediation to
|
|
459
|
+
* announce.
|
|
460
|
+
*
|
|
461
|
+
* `send_resolved` stays off. The dispatcher acts — it re-runs setup and opens a
|
|
462
|
+
* ticket by mail — and a resolution notice would ask it to act again on a
|
|
463
|
+
* condition that has already cleared.
|
|
464
|
+
*
|
|
465
|
+
* Deliveries always carry the bearer token from the mounted Secret: the
|
|
466
|
+
* receiver runs on the host with root-equivalent reach on the edge, so an
|
|
467
|
+
* unauthenticated variant of this route is not a mode worth having.
|
|
468
|
+
* @param {object} params
|
|
469
|
+
* @param {string} params.webhookUrl - Absolute URL of the dispatcher's receiver.
|
|
470
|
+
* @param {string} [params.repeatInterval] - How long before an unresolved alert is redelivered.
|
|
471
|
+
* @returns {string} `alertmanager.yml` contents.
|
|
472
|
+
* @memberof UnderpostMonitoring
|
|
473
|
+
*/
|
|
474
|
+
const alertmanagerConfFactory = ({ webhookUrl, repeatInterval = '1h' } = {}) => {
|
|
475
|
+
const { alertmanager } = UNDERPOST_MONITORING;
|
|
476
|
+
return `global:
|
|
477
|
+
resolve_timeout: 5m
|
|
478
|
+
|
|
479
|
+
route:
|
|
480
|
+
receiver: 'underpost-null'
|
|
481
|
+
# Grouped by subject as well as by event: a fan-out event raises one alert per
|
|
482
|
+
# subject, and grouping them together would deliver a single webhook naming
|
|
483
|
+
# whichever one Alertmanager happened to pick first.
|
|
484
|
+
group_by: ['alertname', 'underpost_event', 'underpost_spoke']
|
|
485
|
+
group_wait: 30s
|
|
486
|
+
group_interval: 5m
|
|
487
|
+
repeat_interval: ${repeatInterval}
|
|
488
|
+
routes:
|
|
489
|
+
- receiver: 'underpost-event'
|
|
490
|
+
matchers:
|
|
491
|
+
- underpost_event =~ ".+"
|
|
492
|
+
|
|
493
|
+
receivers:
|
|
494
|
+
- name: 'underpost-null'
|
|
495
|
+
|
|
496
|
+
- name: 'underpost-event'
|
|
497
|
+
webhook_configs:
|
|
498
|
+
- url: '${webhookUrl}'
|
|
499
|
+
send_resolved: false
|
|
500
|
+
max_alerts: 0
|
|
501
|
+
http_config:
|
|
502
|
+
authorization:
|
|
503
|
+
type: Bearer
|
|
504
|
+
credentials_file: /etc/alertmanager/secret/${alertmanager.tokenKey}
|
|
505
|
+
`;
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* @method blackboxConfFactory
|
|
510
|
+
* @description Renders `blackbox.yml`.
|
|
511
|
+
*
|
|
512
|
+
* The three modules the event registry probes with, and nothing else: a module
|
|
513
|
+
* no probe names is configuration that cannot be observed to be wrong.
|
|
514
|
+
* `insecure_skip_verify` applies to the HTTP prober alone — probes target the
|
|
515
|
+
* edge by address, where the certificate names a hostname it cannot present.
|
|
516
|
+
* @returns {string} `blackbox.yml` contents.
|
|
517
|
+
* @memberof UnderpostMonitoring
|
|
518
|
+
*/
|
|
519
|
+
const blackboxConfFactory = () => `modules:
|
|
520
|
+
http_2xx:
|
|
521
|
+
prober: http
|
|
522
|
+
timeout: 5s
|
|
523
|
+
http:
|
|
524
|
+
preferred_ip_protocol: ip4
|
|
525
|
+
ip_protocol_fallback: false
|
|
526
|
+
valid_status_codes: []
|
|
527
|
+
follow_redirects: true
|
|
528
|
+
tls_config:
|
|
529
|
+
insecure_skip_verify: true
|
|
530
|
+
|
|
531
|
+
tcp_connect:
|
|
532
|
+
prober: tcp
|
|
533
|
+
timeout: 5s
|
|
534
|
+
tcp:
|
|
535
|
+
preferred_ip_protocol: ip4
|
|
536
|
+
ip_protocol_fallback: false
|
|
537
|
+
|
|
538
|
+
icmp:
|
|
539
|
+
prober: icmp
|
|
540
|
+
timeout: 5s
|
|
541
|
+
icmp:
|
|
542
|
+
preferred_ip_protocol: ip4
|
|
543
|
+
ip_protocol_fallback: false
|
|
544
|
+
`;
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* @method grafanaDatasourcesFactory
|
|
548
|
+
* @description Renders the provisioned Prometheus datasource.
|
|
549
|
+
* @param {object} [params]
|
|
550
|
+
* @param {string} [params.namespace] - Namespace holding Prometheus.
|
|
551
|
+
* @returns {string} Datasource provisioning file contents.
|
|
552
|
+
* @memberof UnderpostMonitoring
|
|
553
|
+
*/
|
|
554
|
+
const grafanaDatasourcesFactory = ({ namespace = 'default' } = {}) => {
|
|
555
|
+
const { prometheus } = UNDERPOST_MONITORING;
|
|
556
|
+
return `apiVersion: 1
|
|
557
|
+
datasources:
|
|
558
|
+
- name: Prometheus
|
|
559
|
+
uid: underpost-prometheus
|
|
560
|
+
type: prometheus
|
|
561
|
+
access: proxy
|
|
562
|
+
url: 'http://${serviceDnsFactory(prometheus.name, prometheus.port, namespace)}'
|
|
563
|
+
isDefault: true
|
|
564
|
+
editable: false
|
|
565
|
+
jsonData:
|
|
566
|
+
timeInterval: ${UNDERPOST_MONITORING.scrapeInterval}
|
|
567
|
+
`;
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* @method nodeExporterManifestFactory
|
|
572
|
+
* @description Renders the host-metrics collector as a DaemonSet.
|
|
573
|
+
*
|
|
574
|
+
* `hostNetwork` and `hostPID` because the point is the host, not the pod: CPU,
|
|
575
|
+
* memory, filesystem and interface counters have to describe the machine the
|
|
576
|
+
* workloads run on. The root filesystem is mounted read-only so a collector can
|
|
577
|
+
* never write to what it measures, and the textfile directory is the one path
|
|
578
|
+
* anything else may drop a `.prom` file into.
|
|
579
|
+
* @param {object} [params]
|
|
580
|
+
* @param {string} [params.namespace='default'] - Namespace holding the stack.
|
|
581
|
+
* @returns {string} DaemonSet manifest.
|
|
582
|
+
* @memberof UnderpostMonitoring
|
|
583
|
+
*/
|
|
584
|
+
const nodeExporterManifestFactory = ({ namespace = 'default' } = {}) => {
|
|
585
|
+
const { nodeExporter } = UNDERPOST_MONITORING;
|
|
586
|
+
return `---
|
|
587
|
+
apiVersion: apps/v1
|
|
588
|
+
kind: DaemonSet
|
|
589
|
+
metadata:
|
|
590
|
+
name: ${nodeExporter.name}
|
|
591
|
+
namespace: ${namespace}
|
|
592
|
+
labels:
|
|
593
|
+
app: ${nodeExporter.name}
|
|
594
|
+
spec:
|
|
595
|
+
selector:
|
|
596
|
+
matchLabels:
|
|
597
|
+
app: ${nodeExporter.name}
|
|
598
|
+
template:
|
|
599
|
+
metadata:
|
|
600
|
+
labels:
|
|
601
|
+
app: ${nodeExporter.name}
|
|
602
|
+
spec:
|
|
603
|
+
hostNetwork: true
|
|
604
|
+
hostPID: true
|
|
605
|
+
tolerations:
|
|
606
|
+
- operator: Exists
|
|
607
|
+
containers:
|
|
608
|
+
- name: ${nodeExporter.name}
|
|
609
|
+
image: prom/node-exporter:latest
|
|
610
|
+
args:
|
|
611
|
+
- --path.rootfs=/host/root
|
|
612
|
+
- --path.procfs=/host/proc
|
|
613
|
+
- --path.sysfs=/host/sys
|
|
614
|
+
- --collector.textfile.directory=${nodeExporter.textfileDirectory}
|
|
615
|
+
- --collector.filesystem.mount-points-exclude=${nodeExporter.filesystemExclude}
|
|
616
|
+
ports:
|
|
617
|
+
- name: metrics
|
|
618
|
+
containerPort: ${nodeExporter.port}
|
|
619
|
+
resources:
|
|
620
|
+
requests:
|
|
621
|
+
cpu: 25m
|
|
622
|
+
memory: 32Mi
|
|
623
|
+
securityContext:
|
|
624
|
+
runAsUser: 0
|
|
625
|
+
volumeMounts:
|
|
626
|
+
- name: proc
|
|
627
|
+
mountPath: /host/proc
|
|
628
|
+
readOnly: true
|
|
629
|
+
- name: sys
|
|
630
|
+
mountPath: /host/sys
|
|
631
|
+
readOnly: true
|
|
632
|
+
- name: root
|
|
633
|
+
mountPath: /host/root
|
|
634
|
+
mountPropagation: HostToContainer
|
|
635
|
+
readOnly: true
|
|
636
|
+
- name: textfile
|
|
637
|
+
mountPath: ${nodeExporter.textfileDirectory}
|
|
638
|
+
readOnly: true
|
|
639
|
+
volumes:
|
|
640
|
+
- name: proc
|
|
641
|
+
hostPath:
|
|
642
|
+
path: /proc
|
|
643
|
+
- name: sys
|
|
644
|
+
hostPath:
|
|
645
|
+
path: /sys
|
|
646
|
+
- name: root
|
|
647
|
+
hostPath:
|
|
648
|
+
path: /
|
|
649
|
+
- name: textfile
|
|
650
|
+
hostPath:
|
|
651
|
+
path: ${nodeExporter.textfileDirectory}
|
|
652
|
+
type: DirectoryOrCreate
|
|
653
|
+
`;
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* @method nodeExporterServiceFactory
|
|
658
|
+
* @description Renders the same host collector as a systemd unit, for a machine
|
|
659
|
+
* the cluster cannot schedule a pod onto.
|
|
660
|
+
*
|
|
661
|
+
* Bound to the tunnel address rather than every interface: Prometheus scrapes
|
|
662
|
+
* the hub across WireGuard, and a listener on the VPS public address would
|
|
663
|
+
* publish the host's inventory to the internet. Binding it makes the unit
|
|
664
|
+
* depend on the interface, which is why it is tied to `wg-quick@`.
|
|
665
|
+
* @param {object} params
|
|
666
|
+
* @param {string} params.host - Tunnel address the collector binds.
|
|
667
|
+
* @param {string} [params.interfaceName='wg0'] - Tunnel interface carrying that address.
|
|
668
|
+
* @returns {string} Rendered unit file.
|
|
669
|
+
* @memberof UnderpostMonitoring
|
|
670
|
+
*/
|
|
671
|
+
const nodeExporterServiceFactory = ({ host, interfaceName = 'wg0' }) => {
|
|
672
|
+
const { nodeExporter } = UNDERPOST_MONITORING;
|
|
673
|
+
const tunnelUnit = `wg-quick@${interfaceName}.service`;
|
|
674
|
+
return systemdUnitFactory({
|
|
675
|
+
header:
|
|
676
|
+
'# Generated by `underpost wireguard --node-exporter`. Do not edit by hand:\n' +
|
|
677
|
+
'# the next run rewrites the file and restarts the service.',
|
|
678
|
+
sections: {
|
|
679
|
+
Unit: {
|
|
680
|
+
Description: `Underpost host metrics collector on ${host}:${nodeExporter.port}`,
|
|
681
|
+
Documentation: 'https://www.nexodev.org/docs',
|
|
682
|
+
After: `network-online.target ${tunnelUnit}`,
|
|
683
|
+
Wants: 'network-online.target',
|
|
684
|
+
Requires: tunnelUnit,
|
|
685
|
+
PartOf: tunnelUnit,
|
|
686
|
+
},
|
|
687
|
+
Service: {
|
|
688
|
+
Type: 'simple',
|
|
689
|
+
User: 'root',
|
|
690
|
+
ExecStart: [
|
|
691
|
+
nodeExporter.binaryPath,
|
|
692
|
+
`--web.listen-address=${host}:${nodeExporter.port}`,
|
|
693
|
+
`--collector.textfile.directory=${nodeExporter.textfileDirectory}`,
|
|
694
|
+
`--collector.filesystem.mount-points-exclude=${nodeExporter.filesystemExclude}`,
|
|
695
|
+
].join(' '),
|
|
696
|
+
Restart: 'always',
|
|
697
|
+
RestartSec: 5,
|
|
698
|
+
},
|
|
699
|
+
Install: { WantedBy: `multi-user.target ${tunnelUnit}` },
|
|
700
|
+
},
|
|
701
|
+
});
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* @method nodeExporterServiceScriptFactory
|
|
706
|
+
* @description The single command that provisions the collector on a host.
|
|
707
|
+
*
|
|
708
|
+
* Base64 in, bash out: the remote runner interpolates its command into
|
|
709
|
+
* `bash -lc "..."`, which would eat the quotes, expansions and heredoc this
|
|
710
|
+
* needs. Encoding the script transports it intact and keeps one implementation
|
|
711
|
+
* for the local and the SSH path. The final state read is what makes a failed
|
|
712
|
+
* start a failed run rather than a silent one.
|
|
713
|
+
* @param {object} params
|
|
714
|
+
* @param {string} params.host - Tunnel address the collector binds.
|
|
715
|
+
* @param {string} [params.interfaceName='wg0'] - Tunnel interface carrying that address.
|
|
716
|
+
* @returns {string} Command to execute on the target host.
|
|
717
|
+
* @memberof UnderpostMonitoring
|
|
718
|
+
*/
|
|
719
|
+
const nodeExporterServiceScriptFactory = ({ host, interfaceName = 'wg0' }) => {
|
|
720
|
+
const { nodeExporter } = UNDERPOST_MONITORING;
|
|
721
|
+
const lifecycle = systemdServiceCommandsFactory({
|
|
722
|
+
changed: true,
|
|
723
|
+
name: nodeExporter.serviceName,
|
|
724
|
+
unitPath: nodeExporter.unitPath,
|
|
725
|
+
}).ensure;
|
|
726
|
+
|
|
727
|
+
const script = `set -e
|
|
728
|
+
VERSION=${nodeExporter.version}
|
|
729
|
+
ARCH=$(uname -m)
|
|
730
|
+
case "$ARCH" in x86_64) ARCH=amd64 ;; aarch64) ARCH=arm64 ;; esac
|
|
731
|
+
RELEASE=node_exporter-$VERSION.linux-$ARCH
|
|
732
|
+
# The collector's textfile directory is written by CronJob pods, so it needs the shared container
|
|
733
|
+
# label; the /var/lib default leaves it readable but not writable by container_t. Non-fatal: a
|
|
734
|
+
# host without the SELinux userspace still gets a working collector, just unlabeled.
|
|
735
|
+
# A subshell, not a brace group: the generated mapping command calls exit 1 when semanage is
|
|
736
|
+
# missing, which in a brace group would terminate this installer rather than just this check.
|
|
737
|
+
if ! ( ${containerStorageCommandsFactory(nodeExporter.textfileDirectory)
|
|
738
|
+
.map((command) => `{ ${command}; }`)
|
|
739
|
+
.join(' && ')} ); then
|
|
740
|
+
echo "WARN: could not label ${nodeExporter.textfileDirectory} for container access" >&2
|
|
741
|
+
fi
|
|
742
|
+
if ! ${nodeExporter.binaryPath} --version 2>/dev/null | grep -q "version $VERSION"; then
|
|
743
|
+
TMP=$(mktemp -d)
|
|
744
|
+
curl -fsSL "https://github.com/prometheus/node_exporter/releases/download/v$VERSION/$RELEASE.tar.gz" -o "$TMP/$RELEASE.tar.gz"
|
|
745
|
+
tar -xzf "$TMP/$RELEASE.tar.gz" -C "$TMP"
|
|
746
|
+
sudo install -m 0755 "$TMP/$RELEASE/node_exporter" ${nodeExporter.binaryPath}
|
|
747
|
+
rm -rf "$TMP"
|
|
748
|
+
fi
|
|
749
|
+
sudo tee ${nodeExporter.unitPath} >/dev/null <<'UNIT'
|
|
750
|
+
${nodeExporterServiceFactory({ host, interfaceName })}UNIT
|
|
751
|
+
${lifecycle.join('\n')}
|
|
752
|
+
systemctl is-active --quiet ${nodeExporter.serviceName}
|
|
753
|
+
`;
|
|
754
|
+
|
|
755
|
+
return `echo ${Buffer.from(script, 'utf8').toString('base64')} | base64 -d | bash -s`;
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* @method grafanaDashboardProviderFactory
|
|
760
|
+
* @description Renders the file-based dashboard provider.
|
|
761
|
+
* @returns {string} Dashboard provider provisioning file contents.
|
|
762
|
+
* @memberof UnderpostMonitoring
|
|
763
|
+
*/
|
|
764
|
+
const grafanaDashboardProviderFactory = () => `apiVersion: 1
|
|
765
|
+
providers:
|
|
766
|
+
- name: 'underpost'
|
|
767
|
+
orgId: 1
|
|
768
|
+
folder: 'Underpost'
|
|
769
|
+
type: file
|
|
770
|
+
disableDeletion: false
|
|
771
|
+
allowUiUpdates: true
|
|
772
|
+
updateIntervalSeconds: 30
|
|
773
|
+
options:
|
|
774
|
+
path: ${UNDERPOST_MONITORING.grafana.dashboardsPath}
|
|
775
|
+
foldersFromFilesStructure: false
|
|
776
|
+
`;
|
|
777
|
+
|
|
778
|
+
const timeSeriesPanel = ({ id, title, unit = 'short', gridPos, targets, overrides = [] }) => ({
|
|
779
|
+
id,
|
|
780
|
+
type: 'timeseries',
|
|
781
|
+
title,
|
|
782
|
+
datasource: { type: 'prometheus', uid: 'underpost-prometheus' },
|
|
783
|
+
gridPos,
|
|
784
|
+
fieldConfig: { defaults: { unit, custom: { fillOpacity: 8, lineWidth: 1 } }, overrides },
|
|
785
|
+
options: { legend: { displayMode: 'list', placement: 'bottom' }, tooltip: { mode: 'multi' } },
|
|
786
|
+
targets: targets.map((target, index) => ({ refId: String.fromCharCode(65 + index), ...target })),
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
/** Series identity every host panel groups and labels by. */
|
|
790
|
+
const NODE_LEGEND = '{{instance}} ({{underpost_role}})';
|
|
791
|
+
|
|
792
|
+
/** Re-units one query of a panel and moves it to the opposite axis. */
|
|
793
|
+
const secondaryAxisOverride = (refId, unit) => ({
|
|
794
|
+
matcher: { id: 'byFrameRefID', options: refId },
|
|
795
|
+
properties: [
|
|
796
|
+
{ id: 'unit', value: unit },
|
|
797
|
+
{ id: 'custom.axisPlacement', value: 'right' },
|
|
798
|
+
],
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
const statPanel = ({ id, title, description, unit = 'short', gridPos, targets, defaults = {}, options = {} }) => ({
|
|
802
|
+
id,
|
|
803
|
+
type: 'stat',
|
|
804
|
+
title,
|
|
805
|
+
...(description ? { description } : {}),
|
|
806
|
+
datasource: { type: 'prometheus', uid: 'underpost-prometheus' },
|
|
807
|
+
gridPos,
|
|
808
|
+
fieldConfig: { defaults: { unit, ...defaults }, overrides: [] },
|
|
809
|
+
options: {
|
|
810
|
+
reduceOptions: { calcs: ['lastNotNull'], fields: '', values: false },
|
|
811
|
+
colorMode: 'value',
|
|
812
|
+
...options,
|
|
813
|
+
},
|
|
814
|
+
targets: targets.map((target, index) => ({ refId: String.fromCharCode(65 + index), ...target })),
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* The field config every boolean availability field carries: `probe_success`
|
|
819
|
+
* and `up` are only ever 0 or 1, so the number itself says nothing an operator
|
|
820
|
+
* reads faster than a word and a colour. The threshold step at 1 is what makes
|
|
821
|
+
* anything short of a full success red, and `noValue` separates a target that
|
|
822
|
+
* reports failure from one that stopped reporting at all.
|
|
823
|
+
*/
|
|
824
|
+
const availabilityDefaults = () => ({
|
|
825
|
+
min: 0,
|
|
826
|
+
max: 1,
|
|
827
|
+
decimals: 0,
|
|
828
|
+
noValue: 'NO DATA',
|
|
829
|
+
color: { mode: 'thresholds' },
|
|
830
|
+
mappings: [
|
|
831
|
+
{
|
|
832
|
+
type: 'value',
|
|
833
|
+
options: {
|
|
834
|
+
0: { text: 'DOWN', color: 'red', index: 0 },
|
|
835
|
+
1: { text: 'UP', color: 'green', index: 1 },
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
],
|
|
839
|
+
thresholds: {
|
|
840
|
+
mode: 'absolute',
|
|
841
|
+
steps: [
|
|
842
|
+
{ color: 'red', value: null },
|
|
843
|
+
{ color: 'green', value: 1 },
|
|
844
|
+
],
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* @method stateTimelinePanel
|
|
850
|
+
* @description A boolean availability series as coloured bands over time.
|
|
851
|
+
*
|
|
852
|
+
* A line chart of dozens of targets that are almost always 1 spends its whole
|
|
853
|
+
* vertical axis on two values and draws every target on top of the same line.
|
|
854
|
+
* A state timeline spends the axis on identity instead — one row per series —
|
|
855
|
+
* which is what makes a single red band among fifty green ones findable.
|
|
856
|
+
* Values are merged, so a target that never failed is one band rather than one
|
|
857
|
+
* rectangle per scrape, and nulls are not spanned, so a target that stopped
|
|
858
|
+
* reporting reads as a gap rather than as continued health.
|
|
859
|
+
* @param {object} params
|
|
860
|
+
* @param {number} params.id - Panel id.
|
|
861
|
+
* @param {string} params.title - Panel title.
|
|
862
|
+
* @param {string} [params.description] - Panel description.
|
|
863
|
+
* @param {object} params.gridPos - Grafana grid position.
|
|
864
|
+
* @param {Array<object>} params.targets - Panel queries.
|
|
865
|
+
* @returns {object} Panel model.
|
|
866
|
+
* @memberof UnderpostMonitoring
|
|
867
|
+
*/
|
|
868
|
+
const stateTimelinePanel = ({ id, title, description, gridPos, targets }) => ({
|
|
869
|
+
id,
|
|
870
|
+
type: 'state-timeline',
|
|
871
|
+
title,
|
|
872
|
+
...(description ? { description } : {}),
|
|
873
|
+
datasource: { type: 'prometheus', uid: 'underpost-prometheus' },
|
|
874
|
+
gridPos,
|
|
875
|
+
fieldConfig: {
|
|
876
|
+
defaults: {
|
|
877
|
+
...availabilityDefaults(),
|
|
878
|
+
custom: { fillOpacity: 85, lineWidth: 0, spanNulls: false },
|
|
879
|
+
},
|
|
880
|
+
overrides: [],
|
|
881
|
+
},
|
|
882
|
+
options: {
|
|
883
|
+
mergeValues: true,
|
|
884
|
+
showValue: 'never',
|
|
885
|
+
alignValue: 'left',
|
|
886
|
+
rowHeight: 0.9,
|
|
887
|
+
legend: { showLegend: false, displayMode: 'list', placement: 'bottom' },
|
|
888
|
+
tooltip: { mode: 'single', sort: 'none' },
|
|
889
|
+
},
|
|
890
|
+
targets: targets.map((target, index) => ({ refId: String.fromCharCode(65 + index), ...target })),
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
/** One background-coloured UP/DOWN card per series: the instant state, read at a glance. */
|
|
894
|
+
const availabilityStatPanel = (panel) =>
|
|
895
|
+
statPanel({
|
|
896
|
+
...panel,
|
|
897
|
+
defaults: availabilityDefaults(),
|
|
898
|
+
options: {
|
|
899
|
+
colorMode: 'background',
|
|
900
|
+
graphMode: 'none',
|
|
901
|
+
textMode: 'value_and_name',
|
|
902
|
+
justifyMode: 'center',
|
|
903
|
+
wideLayout: false,
|
|
904
|
+
text: { titleSize: 11, valueSize: 20 },
|
|
905
|
+
},
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
/** Availability as a fraction of the selected range, coloured against an SLO. */
|
|
909
|
+
const uptimeStatPanel = (panel) =>
|
|
910
|
+
statPanel({
|
|
911
|
+
...panel,
|
|
912
|
+
unit: 'percentunit',
|
|
913
|
+
defaults: {
|
|
914
|
+
min: 0,
|
|
915
|
+
max: 1,
|
|
916
|
+
decimals: 3,
|
|
917
|
+
noValue: 'NO DATA',
|
|
918
|
+
color: { mode: 'thresholds' },
|
|
919
|
+
thresholds: {
|
|
920
|
+
mode: 'absolute',
|
|
921
|
+
steps: [
|
|
922
|
+
{ color: 'red', value: null },
|
|
923
|
+
{ color: 'orange', value: 0.99 },
|
|
924
|
+
{ color: 'green', value: 0.999 },
|
|
925
|
+
],
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
options: { colorMode: 'background', graphMode: 'area', textMode: 'value' },
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
/** How many members of a fleet are failing right now; zero reads as a word, not a digit. */
|
|
932
|
+
const outageCountPanel = (panel) =>
|
|
933
|
+
statPanel({
|
|
934
|
+
...panel,
|
|
935
|
+
defaults: {
|
|
936
|
+
decimals: 0,
|
|
937
|
+
color: { mode: 'thresholds' },
|
|
938
|
+
mappings: [{ type: 'value', options: { 0: { text: 'ALL UP', color: 'green', index: 0 } } }],
|
|
939
|
+
thresholds: {
|
|
940
|
+
mode: 'absolute',
|
|
941
|
+
steps: [
|
|
942
|
+
{ color: 'green', value: null },
|
|
943
|
+
{ color: 'red', value: 1 },
|
|
944
|
+
],
|
|
945
|
+
},
|
|
946
|
+
},
|
|
947
|
+
options: { colorMode: 'background', graphMode: 'area', textMode: 'value' },
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
/** A label-values variable, so dozens of targets can be narrowed to the one under investigation. */
|
|
951
|
+
const dashboardVariableFactory = ({ name, label, query }) => ({
|
|
952
|
+
name,
|
|
953
|
+
label,
|
|
954
|
+
type: 'query',
|
|
955
|
+
datasource: { type: 'prometheus', uid: 'underpost-prometheus' },
|
|
956
|
+
definition: query,
|
|
957
|
+
query: { query, refId: `${name}-variable` },
|
|
958
|
+
refresh: 2,
|
|
959
|
+
sort: 1,
|
|
960
|
+
multi: true,
|
|
961
|
+
includeAll: true,
|
|
962
|
+
allValue: '.+',
|
|
963
|
+
current: { selected: true, text: ['All'], value: ['$__all'] },
|
|
964
|
+
options: [],
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* @method envoyDashboardFactory
|
|
969
|
+
* @description The Envoy Gateway data plane dashboard.
|
|
970
|
+
*
|
|
971
|
+
* Downstream 5xx is read from `envoy_http_downstream_rq_xx` filtered to
|
|
972
|
+
* `envoy_response_code_class="5"`. Envoy exposes response classes as one
|
|
973
|
+
* labelled family, not as a `..._rq_5xx` series, so selecting the label is what
|
|
974
|
+
* actually yields the 5xx rate.
|
|
975
|
+
* @returns {string} Dashboard JSON.
|
|
976
|
+
* @memberof UnderpostMonitoring
|
|
977
|
+
*/
|
|
978
|
+
const envoyDashboardFactory = () =>
|
|
979
|
+
`${JSON.stringify(
|
|
980
|
+
{
|
|
981
|
+
uid: 'underpost-envoy',
|
|
982
|
+
title: 'Underpost · Envoy Gateway',
|
|
983
|
+
tags: ['underpost', 'envoy'],
|
|
984
|
+
timezone: 'browser',
|
|
985
|
+
schemaVersion: 39,
|
|
986
|
+
refresh: '30s',
|
|
987
|
+
time: { from: 'now-6h', to: 'now' },
|
|
988
|
+
panels: [
|
|
989
|
+
statPanel({
|
|
990
|
+
id: 1,
|
|
991
|
+
title: 'Data plane uptime',
|
|
992
|
+
unit: 's',
|
|
993
|
+
gridPos: { h: 4, w: 6, x: 0, y: 0 },
|
|
994
|
+
targets: [{ expr: 'max(envoy_server_uptime)', legendFormat: 'uptime' }],
|
|
995
|
+
}),
|
|
996
|
+
statPanel({
|
|
997
|
+
id: 2,
|
|
998
|
+
title: 'Memory allocated',
|
|
999
|
+
unit: 'bytes',
|
|
1000
|
+
gridPos: { h: 4, w: 6, x: 6, y: 0 },
|
|
1001
|
+
targets: [{ expr: 'sum(envoy_server_memory_allocated)', legendFormat: 'allocated' }],
|
|
1002
|
+
}),
|
|
1003
|
+
statPanel({
|
|
1004
|
+
id: 3,
|
|
1005
|
+
title: 'Active downstream connections',
|
|
1006
|
+
gridPos: { h: 4, w: 6, x: 12, y: 0 },
|
|
1007
|
+
targets: [{ expr: 'sum(envoy_http_downstream_cx_active)', legendFormat: 'active' }],
|
|
1008
|
+
}),
|
|
1009
|
+
statPanel({
|
|
1010
|
+
id: 4,
|
|
1011
|
+
title: 'Probe success',
|
|
1012
|
+
gridPos: { h: 4, w: 6, x: 18, y: 0 },
|
|
1013
|
+
targets: [{ expr: 'min(probe_success)', legendFormat: 'probes' }],
|
|
1014
|
+
}),
|
|
1015
|
+
timeSeriesPanel({
|
|
1016
|
+
id: 5,
|
|
1017
|
+
title: 'Downstream requests completed',
|
|
1018
|
+
unit: 'reqps',
|
|
1019
|
+
gridPos: { h: 8, w: 12, x: 0, y: 4 },
|
|
1020
|
+
targets: [
|
|
1021
|
+
{
|
|
1022
|
+
expr: 'sum by (envoy_http_conn_manager_prefix) (rate(envoy_http_downstream_rq_completed[5m]))',
|
|
1023
|
+
legendFormat: '{{envoy_http_conn_manager_prefix}}',
|
|
1024
|
+
},
|
|
1025
|
+
],
|
|
1026
|
+
}),
|
|
1027
|
+
timeSeriesPanel({
|
|
1028
|
+
id: 6,
|
|
1029
|
+
title: 'Downstream 5xx',
|
|
1030
|
+
unit: 'reqps',
|
|
1031
|
+
gridPos: { h: 8, w: 12, x: 12, y: 4 },
|
|
1032
|
+
targets: [
|
|
1033
|
+
{
|
|
1034
|
+
expr: 'sum by (envoy_http_conn_manager_prefix) (rate(envoy_http_downstream_rq_xx{envoy_response_code_class="5"}[5m]))',
|
|
1035
|
+
legendFormat: '{{envoy_http_conn_manager_prefix}} 5xx',
|
|
1036
|
+
},
|
|
1037
|
+
],
|
|
1038
|
+
}),
|
|
1039
|
+
timeSeriesPanel({
|
|
1040
|
+
id: 7,
|
|
1041
|
+
title: 'Downstream throughput',
|
|
1042
|
+
unit: 'Bps',
|
|
1043
|
+
gridPos: { h: 8, w: 12, x: 0, y: 12 },
|
|
1044
|
+
targets: [
|
|
1045
|
+
{ expr: 'sum(rate(envoy_http_downstream_cx_rx_bytes_total[5m]))', legendFormat: 'rx' },
|
|
1046
|
+
{ expr: 'sum(rate(envoy_http_downstream_cx_tx_bytes_total[5m]))', legendFormat: 'tx' },
|
|
1047
|
+
],
|
|
1048
|
+
}),
|
|
1049
|
+
timeSeriesPanel({
|
|
1050
|
+
id: 8,
|
|
1051
|
+
title: 'Active connections',
|
|
1052
|
+
gridPos: { h: 8, w: 12, x: 12, y: 12 },
|
|
1053
|
+
targets: [
|
|
1054
|
+
{
|
|
1055
|
+
expr: 'sum by (envoy_http_conn_manager_prefix) (envoy_http_downstream_cx_active)',
|
|
1056
|
+
legendFormat: '{{envoy_http_conn_manager_prefix}}',
|
|
1057
|
+
},
|
|
1058
|
+
],
|
|
1059
|
+
}),
|
|
1060
|
+
],
|
|
1061
|
+
},
|
|
1062
|
+
null,
|
|
1063
|
+
2,
|
|
1064
|
+
)}\n`;
|
|
1065
|
+
|
|
1066
|
+
/** Series the availability panels read, narrowed by the dashboard's own variables. */
|
|
1067
|
+
const PROBE_SELECTOR = 'probe_success{underpost_event=~"$event"}';
|
|
1068
|
+
const TARGET_SELECTOR = 'up{job=~"$job"}';
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* A step of a state timeline is wider than a scrape as soon as the range is,
|
|
1072
|
+
* and Prometheus answers each step with its last sample — which is how a short
|
|
1073
|
+
* outage disappears from a week-long view. Reducing the step with `min_over_time`
|
|
1074
|
+
* keeps the failure: any zero inside the window colours the whole band red.
|
|
1075
|
+
* `$__rate_interval` rather than `$__interval` because it is guaranteed to be
|
|
1076
|
+
* wider than the scrape interval, so the reduction can never see an empty range.
|
|
1077
|
+
*/
|
|
1078
|
+
const timelineExpr = (selector) => `min_over_time(${selector}[$__rate_interval])`;
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* @method eventsDashboardFactory
|
|
1082
|
+
* @description The event/probe dashboard: what each registered event's probes
|
|
1083
|
+
* report, and what the Express runtimes are serving.
|
|
1084
|
+
*
|
|
1085
|
+
* Availability is boolean, and the dashboard is laid out the way an operator
|
|
1086
|
+
* reads one: the top row answers "is anything broken right now, and how much
|
|
1087
|
+
* has it cost", the card grids name which subject is broken, and the state
|
|
1088
|
+
* timelines say when it broke and for how long. Nothing renders 0/1 on a
|
|
1089
|
+
* numeric axis, because dozens of targets that are almost always up produce a
|
|
1090
|
+
* single line and an unreadable legend.
|
|
1091
|
+
* @returns {string} Dashboard JSON.
|
|
1092
|
+
* @memberof UnderpostMonitoring
|
|
1093
|
+
*/
|
|
1094
|
+
const eventsDashboardFactory = () =>
|
|
1095
|
+
`${JSON.stringify(
|
|
1096
|
+
{
|
|
1097
|
+
uid: 'underpost-events',
|
|
1098
|
+
title: 'Underpost · Events and Probes',
|
|
1099
|
+
tags: ['underpost', 'events'],
|
|
1100
|
+
timezone: 'browser',
|
|
1101
|
+
schemaVersion: 39,
|
|
1102
|
+
refresh: '30s',
|
|
1103
|
+
time: { from: 'now-6h', to: 'now' },
|
|
1104
|
+
templating: {
|
|
1105
|
+
list: [
|
|
1106
|
+
dashboardVariableFactory({
|
|
1107
|
+
name: 'event',
|
|
1108
|
+
label: 'Event',
|
|
1109
|
+
query: 'label_values(probe_success, underpost_event)',
|
|
1110
|
+
}),
|
|
1111
|
+
dashboardVariableFactory({ name: 'job', label: 'Job', query: 'label_values(up, job)' }),
|
|
1112
|
+
],
|
|
1113
|
+
},
|
|
1114
|
+
panels: [
|
|
1115
|
+
outageCountPanel({
|
|
1116
|
+
id: 10,
|
|
1117
|
+
title: 'Probes failing now',
|
|
1118
|
+
description: 'Blackbox probes reporting 0 on their most recent scrape.',
|
|
1119
|
+
gridPos: { h: 4, w: 6, x: 0, y: 0 },
|
|
1120
|
+
targets: [{ expr: `count(${PROBE_SELECTOR} == 0) or vector(0)`, legendFormat: 'failing', instant: true }],
|
|
1121
|
+
}),
|
|
1122
|
+
uptimeStatPanel({
|
|
1123
|
+
id: 11,
|
|
1124
|
+
title: 'Probe availability · range',
|
|
1125
|
+
description: 'Mean probe success over the selected time range, across every probe.',
|
|
1126
|
+
gridPos: { h: 4, w: 6, x: 6, y: 0 },
|
|
1127
|
+
targets: [
|
|
1128
|
+
{ expr: `avg(avg_over_time(${PROBE_SELECTOR}[$__range]))`, legendFormat: 'availability', instant: true },
|
|
1129
|
+
],
|
|
1130
|
+
}),
|
|
1131
|
+
outageCountPanel({
|
|
1132
|
+
id: 12,
|
|
1133
|
+
title: 'Targets failing now',
|
|
1134
|
+
description: 'Scrape targets Prometheus could not reach on its most recent attempt.',
|
|
1135
|
+
gridPos: { h: 4, w: 6, x: 12, y: 0 },
|
|
1136
|
+
targets: [{ expr: `count(${TARGET_SELECTOR} == 0) or vector(0)`, legendFormat: 'failing', instant: true }],
|
|
1137
|
+
}),
|
|
1138
|
+
uptimeStatPanel({
|
|
1139
|
+
id: 13,
|
|
1140
|
+
title: 'Target availability · range',
|
|
1141
|
+
description: 'Worst single target over the selected range, not the fleet average.',
|
|
1142
|
+
gridPos: { h: 4, w: 6, x: 18, y: 0 },
|
|
1143
|
+
targets: [
|
|
1144
|
+
{
|
|
1145
|
+
expr: `min(avg_over_time(${TARGET_SELECTOR}[$__range]))`,
|
|
1146
|
+
legendFormat: 'worst target',
|
|
1147
|
+
instant: true,
|
|
1148
|
+
},
|
|
1149
|
+
],
|
|
1150
|
+
}),
|
|
1151
|
+
availabilityStatPanel({
|
|
1152
|
+
id: 14,
|
|
1153
|
+
title: 'Probe status by event',
|
|
1154
|
+
gridPos: { h: 5, w: 24, x: 0, y: 4 },
|
|
1155
|
+
targets: [{ expr: PROBE_SELECTOR, legendFormat: '{{underpost_event}} · {{instance}}', instant: true }],
|
|
1156
|
+
}),
|
|
1157
|
+
stateTimelinePanel({
|
|
1158
|
+
id: 1,
|
|
1159
|
+
title: 'Probe success by event',
|
|
1160
|
+
gridPos: { h: 9, w: 24, x: 0, y: 9 },
|
|
1161
|
+
targets: [{ expr: timelineExpr(PROBE_SELECTOR), legendFormat: '{{underpost_event}} · {{instance}}' }],
|
|
1162
|
+
}),
|
|
1163
|
+
availabilityStatPanel({
|
|
1164
|
+
id: 15,
|
|
1165
|
+
title: 'Target status',
|
|
1166
|
+
gridPos: { h: 5, w: 24, x: 0, y: 18 },
|
|
1167
|
+
targets: [{ expr: TARGET_SELECTOR, legendFormat: '{{job}} · {{instance}}', instant: true }],
|
|
1168
|
+
}),
|
|
1169
|
+
stateTimelinePanel({
|
|
1170
|
+
id: 4,
|
|
1171
|
+
title: 'Target availability',
|
|
1172
|
+
gridPos: { h: 9, w: 24, x: 0, y: 23 },
|
|
1173
|
+
targets: [{ expr: timelineExpr(TARGET_SELECTOR), legendFormat: '{{job}} · {{instance}}' }],
|
|
1174
|
+
}),
|
|
1175
|
+
timeSeriesPanel({
|
|
1176
|
+
id: 2,
|
|
1177
|
+
title: 'Probe duration',
|
|
1178
|
+
unit: 's',
|
|
1179
|
+
gridPos: { h: 8, w: 12, x: 0, y: 32 },
|
|
1180
|
+
targets: [
|
|
1181
|
+
{
|
|
1182
|
+
expr: 'probe_duration_seconds{underpost_event=~"$event"}',
|
|
1183
|
+
legendFormat: '{{underpost_event}} · {{instance}}',
|
|
1184
|
+
},
|
|
1185
|
+
],
|
|
1186
|
+
}),
|
|
1187
|
+
timeSeriesPanel({
|
|
1188
|
+
id: 3,
|
|
1189
|
+
title: 'Express HTTP requests',
|
|
1190
|
+
unit: 'reqps',
|
|
1191
|
+
gridPos: { h: 8, w: 12, x: 12, y: 32 },
|
|
1192
|
+
targets: [
|
|
1193
|
+
{
|
|
1194
|
+
expr: 'sum by (job, instance) (rate({__name__=~".+_http_requests_total"}[5m]))',
|
|
1195
|
+
legendFormat: '{{instance}}',
|
|
1196
|
+
},
|
|
1197
|
+
],
|
|
1198
|
+
}),
|
|
1199
|
+
],
|
|
1200
|
+
},
|
|
1201
|
+
null,
|
|
1202
|
+
2,
|
|
1203
|
+
)}\n`;
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* @method grafanaExposureFactory
|
|
1207
|
+
* @description The URL model Grafana must be told about for a given exposure.
|
|
1208
|
+
*
|
|
1209
|
+
* Grafana builds every asset link, redirect and OAuth callback from
|
|
1210
|
+
* `root_url`, so it has to be told where it is being reached — a dashboard
|
|
1211
|
+
* served at a sub-path with the default root URL loads a blank page whose
|
|
1212
|
+
* assets 404 against the domain root. The two exposures need opposite answers,
|
|
1213
|
+
* which is why this resolves to a pair rather than a constant:
|
|
1214
|
+
*
|
|
1215
|
+
* - behind the edge at `/grafana`, it serves from the sub-path and must keep
|
|
1216
|
+
* the prefix, so no route rewrite is involved;
|
|
1217
|
+
* - on a NodePort it owns the whole origin and serves from the root.
|
|
1218
|
+
* @param {object} [params]
|
|
1219
|
+
* @param {string} [params.host] - Hostname the edge serves it under.
|
|
1220
|
+
* @param {string} [params.nodeIp] - Node address for the NodePort exposure.
|
|
1221
|
+
* @param {number} [params.nodePort] - Node port.
|
|
1222
|
+
* @returns {{rootUrl: string, subPath: boolean, url: string}} Root URL, sub-path mode, and the browser URL.
|
|
1223
|
+
* @memberof UnderpostMonitoring
|
|
1224
|
+
*/
|
|
1225
|
+
const grafanaExposureFactory = ({ host = '', nodeIp = '', nodePort = UNDERPOST_MONITORING.grafana.nodePort } = {}) => {
|
|
1226
|
+
const { subPath, port } = UNDERPOST_MONITORING.grafana;
|
|
1227
|
+
if (host) {
|
|
1228
|
+
const url = `https://${host}${subPath}`;
|
|
1229
|
+
return { rootUrl: `${url}/`, subPath: true, url };
|
|
1230
|
+
}
|
|
1231
|
+
if (nodeIp) {
|
|
1232
|
+
const url = `http://${nodeIp}:${nodePort}`;
|
|
1233
|
+
return { rootUrl: `${url}/`, subPath: false, url };
|
|
1234
|
+
}
|
|
1235
|
+
return { rootUrl: `http://localhost:${port}/`, subPath: false, url: `http://localhost:${port}` };
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
const grafanaStorageResetRequiredFactory = ({ nodeName = '', volumeNodes = [] } = {}) =>
|
|
1239
|
+
Boolean(nodeName && volumeNodes.length > 0 && !volumeNodes.includes(nodeName));
|
|
1240
|
+
|
|
1241
|
+
const grafanaResetPlanFactory = (volumeName = '') => {
|
|
1242
|
+
const { name, nodePortName, pvcName, routeName } = UNDERPOST_MONITORING.grafana;
|
|
1243
|
+
return {
|
|
1244
|
+
controllers: [`deployment/${name}`, `statefulset/${name}`],
|
|
1245
|
+
resources: [`service/${name}`, `service/${nodePortName}`, `httproute/${routeName}`],
|
|
1246
|
+
claim: `pvc/${pvcName}`,
|
|
1247
|
+
volumeName,
|
|
1248
|
+
};
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
const grafanaAdminEnvFactory = () => [
|
|
1252
|
+
{
|
|
1253
|
+
name: 'GF_SECURITY_ADMIN_USER',
|
|
1254
|
+
valueFrom: {
|
|
1255
|
+
secretKeyRef: {
|
|
1256
|
+
name: UNDERPOST_MONITORING.grafana.adminSecretName,
|
|
1257
|
+
key: UNDERPOST_MONITORING.grafana.adminUserKey,
|
|
1258
|
+
},
|
|
1259
|
+
},
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
name: 'GF_SECURITY_ADMIN_PASSWORD',
|
|
1263
|
+
valueFrom: {
|
|
1264
|
+
secretKeyRef: {
|
|
1265
|
+
name: UNDERPOST_MONITORING.grafana.adminSecretName,
|
|
1266
|
+
key: UNDERPOST_MONITORING.grafana.adminPasswordKey,
|
|
1267
|
+
},
|
|
1268
|
+
},
|
|
1269
|
+
},
|
|
1270
|
+
];
|
|
1271
|
+
const grafanaRuntimeEnvFactory = (exposure) => [
|
|
1272
|
+
{ name: 'GF_SERVER_ROOT_URL', value: exposure.rootUrl },
|
|
1273
|
+
{ name: 'GF_SERVER_SERVE_FROM_SUB_PATH', value: `${exposure.subPath}` },
|
|
1274
|
+
];
|
|
1275
|
+
const grafanaManagedEnvFactory = (exposure) => [...grafanaAdminEnvFactory(), ...grafanaRuntimeEnvFactory(exposure)];
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* @method grafanaDeploymentPatchFactory
|
|
1279
|
+
* @description Renders the runtime fields used when republishing an existing Grafana Deployment.
|
|
1280
|
+
* @param {object} params
|
|
1281
|
+
* @param {object} params.exposure - Result of {@link grafanaExposureFactory}.
|
|
1282
|
+
* @param {string} [params.nodeName] - Kubernetes node to pin Grafana to.
|
|
1283
|
+
* @param {string} [params.adminSecretVersion] - Secret resource version used to roll Grafana.
|
|
1284
|
+
* @returns {object} Kubernetes Deployment merge patch.
|
|
1285
|
+
* @memberof UnderpostMonitoring
|
|
1286
|
+
*/
|
|
1287
|
+
const grafanaDeploymentPatchFactory = ({ exposure, nodeName = '', adminSecretVersion = '' } = {}) => ({
|
|
1288
|
+
spec: {
|
|
1289
|
+
template: {
|
|
1290
|
+
...(adminSecretVersion
|
|
1291
|
+
? {
|
|
1292
|
+
metadata: {
|
|
1293
|
+
annotations: {
|
|
1294
|
+
[UNDERPOST_MONITORING.grafana.adminSecretVersionAnnotation]: adminSecretVersion,
|
|
1295
|
+
},
|
|
1296
|
+
},
|
|
1297
|
+
}
|
|
1298
|
+
: {}),
|
|
1299
|
+
spec: {
|
|
1300
|
+
...(nodeName ? { nodeSelector: { 'kubernetes.io/hostname': nodeName } } : {}),
|
|
1301
|
+
containers: [
|
|
1302
|
+
{
|
|
1303
|
+
name: UNDERPOST_MONITORING.grafana.name,
|
|
1304
|
+
env: [...(adminSecretVersion ? grafanaAdminEnvFactory() : []), ...grafanaRuntimeEnvFactory(exposure)],
|
|
1305
|
+
},
|
|
1306
|
+
],
|
|
1307
|
+
},
|
|
1308
|
+
},
|
|
1309
|
+
},
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* @method grafanaDeploymentFactory
|
|
1314
|
+
* @description Adds the browser origin and placement before Grafana's first pod is created.
|
|
1315
|
+
* @param {object} params
|
|
1316
|
+
* @param {object} params.deployment - Parsed base Deployment manifest.
|
|
1317
|
+
* @param {object} params.exposure - Result of {@link grafanaExposureFactory}.
|
|
1318
|
+
* @param {string} [params.namespace='default'] - Namespace holding Grafana.
|
|
1319
|
+
* @param {string} [params.nodeName] - Kubernetes node to pin Grafana to.
|
|
1320
|
+
* @param {string} [params.adminSecretVersion] - Secret resource version used to roll Grafana.
|
|
1321
|
+
* @returns {object} Deployment ready to apply.
|
|
1322
|
+
* @memberof UnderpostMonitoring
|
|
1323
|
+
*/
|
|
1324
|
+
const grafanaDeploymentFactory = ({
|
|
1325
|
+
deployment,
|
|
1326
|
+
exposure,
|
|
1327
|
+
namespace = 'default',
|
|
1328
|
+
nodeName = '',
|
|
1329
|
+
adminSecretVersion = '',
|
|
1330
|
+
} = {}) => {
|
|
1331
|
+
const rendered = structuredClone(deployment);
|
|
1332
|
+
rendered.metadata.namespace = namespace;
|
|
1333
|
+
if (adminSecretVersion) {
|
|
1334
|
+
rendered.spec.template.metadata ||= {};
|
|
1335
|
+
rendered.spec.template.metadata.annotations = {
|
|
1336
|
+
...(rendered.spec.template.metadata.annotations || {}),
|
|
1337
|
+
[UNDERPOST_MONITORING.grafana.adminSecretVersionAnnotation]: adminSecretVersion,
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
const podSpec = rendered.spec.template.spec;
|
|
1341
|
+
podSpec.nodeSelector = nodeName ? { 'kubernetes.io/hostname': nodeName } : {};
|
|
1342
|
+
|
|
1343
|
+
const container = podSpec.containers.find(({ name }) => name === UNDERPOST_MONITORING.grafana.name);
|
|
1344
|
+
if (!container) throw new Error('Grafana Deployment manifest has no grafana container');
|
|
1345
|
+
const managedNames = new Set(grafanaManagedEnvFactory(exposure).map(({ name }) => name));
|
|
1346
|
+
container.env = [
|
|
1347
|
+
...(container.env || []).filter(({ name }) => !managedNames.has(name)),
|
|
1348
|
+
...grafanaManagedEnvFactory(exposure),
|
|
1349
|
+
];
|
|
1350
|
+
return rendered;
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1353
|
+
/** Renders the Secret used for Grafana's initial administrator credentials. */
|
|
1354
|
+
const grafanaAdminSecretFactory = ({ username, password, namespace = 'default' } = {}) => {
|
|
1355
|
+
if (!username || !password) throw new Error('Grafana admin username and password are required');
|
|
1356
|
+
const { name, adminSecretName, adminUserKey, adminPasswordKey } = UNDERPOST_MONITORING.grafana;
|
|
1357
|
+
return `
|
|
1358
|
+
---
|
|
1359
|
+
apiVersion: v1
|
|
1360
|
+
kind: Secret
|
|
1361
|
+
metadata:
|
|
1362
|
+
name: ${adminSecretName}
|
|
1363
|
+
namespace: ${namespace}
|
|
1364
|
+
labels:
|
|
1365
|
+
app: ${name}
|
|
1366
|
+
app.kubernetes.io/managed-by: underpost
|
|
1367
|
+
type: Opaque
|
|
1368
|
+
data:
|
|
1369
|
+
${adminUserKey}: ${Buffer.from(`${username}`, 'utf8').toString('base64')}
|
|
1370
|
+
${adminPasswordKey}: ${Buffer.from(`${password}`, 'utf8').toString('base64')}
|
|
1371
|
+
`;
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* @method grafanaNodePortFactory
|
|
1376
|
+
* @description Renders the NodePort Service that publishes Grafana on the LAN.
|
|
1377
|
+
*
|
|
1378
|
+
* A second Service beside the ClusterIP rather than a change to it: the
|
|
1379
|
+
* in-cluster datasource and the browser reach Grafana for different reasons,
|
|
1380
|
+
* and a NodePort withdrawn later must not take the cluster-internal address
|
|
1381
|
+
* with it.
|
|
1382
|
+
* @param {object} [params]
|
|
1383
|
+
* @param {string} [params.namespace] - Namespace holding Grafana.
|
|
1384
|
+
* @param {number} [params.nodePort] - Node port to publish on.
|
|
1385
|
+
* @returns {string} Service YAML.
|
|
1386
|
+
* @memberof UnderpostMonitoring
|
|
1387
|
+
*/
|
|
1388
|
+
const grafanaNodePortFactory = ({ namespace = 'default', nodePort = UNDERPOST_MONITORING.grafana.nodePort } = {}) => {
|
|
1389
|
+
const { name, nodePortName, port } = UNDERPOST_MONITORING.grafana;
|
|
1390
|
+
return `
|
|
1391
|
+
---
|
|
1392
|
+
apiVersion: v1
|
|
1393
|
+
kind: Service
|
|
1394
|
+
metadata:
|
|
1395
|
+
name: ${nodePortName}
|
|
1396
|
+
namespace: ${namespace}
|
|
1397
|
+
labels:
|
|
1398
|
+
app: ${name}
|
|
1399
|
+
spec:
|
|
1400
|
+
type: NodePort
|
|
1401
|
+
externalTrafficPolicy: Cluster
|
|
1402
|
+
selector:
|
|
1403
|
+
app: ${name}
|
|
1404
|
+
ports:
|
|
1405
|
+
- name: http
|
|
1406
|
+
protocol: TCP
|
|
1407
|
+
port: ${port}
|
|
1408
|
+
targetPort: ${port}
|
|
1409
|
+
nodePort: ${nodePort}
|
|
1410
|
+
`;
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
/**
|
|
1414
|
+
* @method nodeMetricsDashboardFactory
|
|
1415
|
+
* @description The host-metrics dashboard.
|
|
1416
|
+
*
|
|
1417
|
+
* Every panel is grouped by `instance`, which for a discovered node is its
|
|
1418
|
+
* InternalIP and for the hub its tunnel address — the same identity the node
|
|
1419
|
+
* events resolve a repair target from, so a spike here names a machine an
|
|
1420
|
+
* operator can act on, and legends carry the `underpost_role` Prometheus
|
|
1421
|
+
* relabels onto it so hub, control and worker are told apart at a glance.
|
|
1422
|
+
* @returns {string} Dashboard JSON.
|
|
1423
|
+
* @memberof UnderpostMonitoring
|
|
1424
|
+
*/
|
|
1425
|
+
const nodeMetricsDashboardFactory = () => {
|
|
1426
|
+
const { networkDeviceSelector } = UNDERPOST_MONITORING.nodeExporter;
|
|
1427
|
+
return `${JSON.stringify(
|
|
1428
|
+
{
|
|
1429
|
+
uid: 'underpost-node-metrics',
|
|
1430
|
+
title: 'Underpost · Node Metrics',
|
|
1431
|
+
tags: ['underpost', 'nodes', 'hardware'],
|
|
1432
|
+
timezone: 'browser',
|
|
1433
|
+
schemaVersion: 39,
|
|
1434
|
+
refresh: '30s',
|
|
1435
|
+
time: { from: 'now-6h', to: 'now' },
|
|
1436
|
+
panels: [
|
|
1437
|
+
timeSeriesPanel({
|
|
1438
|
+
id: 1,
|
|
1439
|
+
title: 'Node CPU Usage %',
|
|
1440
|
+
unit: 'percent',
|
|
1441
|
+
gridPos: { h: 8, w: 12, x: 0, y: 0 },
|
|
1442
|
+
targets: [
|
|
1443
|
+
{
|
|
1444
|
+
expr:
|
|
1445
|
+
'100 - (avg by (instance, underpost_role) ' + '(rate(node_cpu_seconds_total{mode="idle"}[2m])) * 100)',
|
|
1446
|
+
legendFormat: NODE_LEGEND,
|
|
1447
|
+
},
|
|
1448
|
+
],
|
|
1449
|
+
}),
|
|
1450
|
+
timeSeriesPanel({
|
|
1451
|
+
id: 2,
|
|
1452
|
+
title: 'Node Memory Usage %',
|
|
1453
|
+
unit: 'percent',
|
|
1454
|
+
gridPos: { h: 8, w: 12, x: 12, y: 0 },
|
|
1455
|
+
targets: [
|
|
1456
|
+
{
|
|
1457
|
+
expr: '100 * (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes))',
|
|
1458
|
+
legendFormat: NODE_LEGEND,
|
|
1459
|
+
},
|
|
1460
|
+
],
|
|
1461
|
+
}),
|
|
1462
|
+
timeSeriesPanel({
|
|
1463
|
+
id: 3,
|
|
1464
|
+
title: 'Network Throughput (RX/TX)',
|
|
1465
|
+
unit: 'binBps',
|
|
1466
|
+
gridPos: { h: 8, w: 12, x: 0, y: 8 },
|
|
1467
|
+
targets: [
|
|
1468
|
+
{
|
|
1469
|
+
expr: `rate(node_network_receive_bytes_total{${networkDeviceSelector}}[2m])`,
|
|
1470
|
+
legendFormat: `${NODE_LEGEND} {{device}} rx`,
|
|
1471
|
+
},
|
|
1472
|
+
{
|
|
1473
|
+
expr: `rate(node_network_transmit_bytes_total{${networkDeviceSelector}}[2m])`,
|
|
1474
|
+
legendFormat: `${NODE_LEGEND} {{device}} tx`,
|
|
1475
|
+
},
|
|
1476
|
+
],
|
|
1477
|
+
}),
|
|
1478
|
+
timeSeriesPanel({
|
|
1479
|
+
id: 4,
|
|
1480
|
+
title: 'Disk Usage % & I/O Rates',
|
|
1481
|
+
unit: 'percent',
|
|
1482
|
+
gridPos: { h: 8, w: 12, x: 12, y: 8 },
|
|
1483
|
+
overrides: [secondaryAxisOverride('B', 'binBps'), secondaryAxisOverride('C', 'binBps')],
|
|
1484
|
+
targets: [
|
|
1485
|
+
{
|
|
1486
|
+
expr:
|
|
1487
|
+
'100 - ((node_filesystem_avail_bytes{mountpoint="/"} * 100) / ' +
|
|
1488
|
+
'node_filesystem_size_bytes{mountpoint="/"})',
|
|
1489
|
+
legendFormat: `${NODE_LEGEND} used %`,
|
|
1490
|
+
},
|
|
1491
|
+
{ expr: 'rate(node_disk_read_bytes_total[2m])', legendFormat: `${NODE_LEGEND} {{device}} read` },
|
|
1492
|
+
{ expr: 'rate(node_disk_written_bytes_total[2m])', legendFormat: `${NODE_LEGEND} {{device}} write` },
|
|
1493
|
+
],
|
|
1494
|
+
}),
|
|
1495
|
+
timeSeriesPanel({
|
|
1496
|
+
id: 5,
|
|
1497
|
+
title: 'Vultr Hub Monthly Bandwidth Usage',
|
|
1498
|
+
unit: 'percent',
|
|
1499
|
+
gridPos: { h: 6, w: 24, x: 0, y: 16 },
|
|
1500
|
+
targets: [
|
|
1501
|
+
{
|
|
1502
|
+
expr: '(vultr_bandwidth_used_bytes / vultr_bandwidth_limit_bytes) * 100',
|
|
1503
|
+
legendFormat: 'Hub Bandwidth Usage % (hub)',
|
|
1504
|
+
},
|
|
1505
|
+
],
|
|
1506
|
+
}),
|
|
1507
|
+
],
|
|
1508
|
+
},
|
|
1509
|
+
null,
|
|
1510
|
+
2,
|
|
1511
|
+
)}\n`;
|
|
1512
|
+
};
|
|
1513
|
+
|
|
1514
|
+
/**
|
|
1515
|
+
* @method monitoringConfigFactory
|
|
1516
|
+
* @description Renders the namespace-dependent half of the stack: the RBAC that
|
|
1517
|
+
* lets Prometheus discover pods, and every ConfigMap the four components mount.
|
|
1518
|
+
*
|
|
1519
|
+
* Configuration is generated and workloads are not: the Deployments in
|
|
1520
|
+
* `manifests/` describe processes that do not change, while these documents are
|
|
1521
|
+
* a projection of the live deploy configuration and event registry. Applying
|
|
1522
|
+
* them is what makes a newly added host or event visible to the stack. The RBAC
|
|
1523
|
+
* belongs here rather than in the file because a ClusterRoleBinding names its
|
|
1524
|
+
* ServiceAccount's namespace in the document body, where `kubectl -n` does not
|
|
1525
|
+
* reach.
|
|
1526
|
+
* @param {object} params
|
|
1527
|
+
* @param {string} [params.namespace] - Namespace to render into.
|
|
1528
|
+
* @param {string} params.prometheusConf - Rendered `prometheus.yml`.
|
|
1529
|
+
* @param {string} params.alertRules - Rendered rules file.
|
|
1530
|
+
* @param {string} params.alertmanagerConf - Rendered `alertmanager.yml`.
|
|
1531
|
+
* @param {string} params.blackboxConf - Rendered `blackbox.yml`.
|
|
1532
|
+
* @returns {string} Multi-document YAML.
|
|
1533
|
+
* @memberof UnderpostMonitoring
|
|
1534
|
+
*/
|
|
1535
|
+
const monitoringConfigFactory = ({
|
|
1536
|
+
namespace = 'default',
|
|
1537
|
+
prometheusConf,
|
|
1538
|
+
alertRules,
|
|
1539
|
+
alertmanagerConf,
|
|
1540
|
+
blackboxConf,
|
|
1541
|
+
} = {}) => {
|
|
1542
|
+
const { prometheus, alertmanager, blackbox, grafana } = UNDERPOST_MONITORING;
|
|
1543
|
+
return `${nodeExporterManifestFactory({ namespace })}
|
|
1544
|
+
---
|
|
1545
|
+
apiVersion: v1
|
|
1546
|
+
kind: ServiceAccount
|
|
1547
|
+
metadata:
|
|
1548
|
+
name: ${prometheus.name}
|
|
1549
|
+
namespace: ${namespace}
|
|
1550
|
+
labels:
|
|
1551
|
+
app: ${prometheus.name}
|
|
1552
|
+
---
|
|
1553
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
1554
|
+
kind: ClusterRole
|
|
1555
|
+
metadata:
|
|
1556
|
+
name: ${prometheus.name}
|
|
1557
|
+
labels:
|
|
1558
|
+
app: ${prometheus.name}
|
|
1559
|
+
rules:
|
|
1560
|
+
- apiGroups: ['']
|
|
1561
|
+
resources: ['nodes', 'nodes/metrics', 'services', 'endpoints', 'pods']
|
|
1562
|
+
verbs: ['get', 'list', 'watch']
|
|
1563
|
+
- nonResourceURLs: ['/metrics']
|
|
1564
|
+
verbs: ['get']
|
|
1565
|
+
---
|
|
1566
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
1567
|
+
kind: ClusterRoleBinding
|
|
1568
|
+
metadata:
|
|
1569
|
+
name: ${prometheus.name}
|
|
1570
|
+
labels:
|
|
1571
|
+
app: ${prometheus.name}
|
|
1572
|
+
roleRef:
|
|
1573
|
+
apiGroup: rbac.authorization.k8s.io
|
|
1574
|
+
kind: ClusterRole
|
|
1575
|
+
name: ${prometheus.name}
|
|
1576
|
+
subjects:
|
|
1577
|
+
- kind: ServiceAccount
|
|
1578
|
+
name: ${prometheus.name}
|
|
1579
|
+
namespace: ${namespace}
|
|
1580
|
+
---
|
|
1581
|
+
apiVersion: v1
|
|
1582
|
+
kind: ConfigMap
|
|
1583
|
+
metadata:
|
|
1584
|
+
name: ${prometheus.configMapName}
|
|
1585
|
+
namespace: ${namespace}
|
|
1586
|
+
labels:
|
|
1587
|
+
app: ${prometheus.name}
|
|
1588
|
+
data:
|
|
1589
|
+
prometheus.yml: |
|
|
1590
|
+
${indentBlock(prometheusConf, 4)}
|
|
1591
|
+
---
|
|
1592
|
+
apiVersion: v1
|
|
1593
|
+
kind: ConfigMap
|
|
1594
|
+
metadata:
|
|
1595
|
+
name: ${prometheus.rulesConfigMapName}
|
|
1596
|
+
namespace: ${namespace}
|
|
1597
|
+
labels:
|
|
1598
|
+
app: ${prometheus.name}
|
|
1599
|
+
data:
|
|
1600
|
+
underpost-events.yml: |
|
|
1601
|
+
${indentBlock(alertRules, 4)}
|
|
1602
|
+
---
|
|
1603
|
+
apiVersion: v1
|
|
1604
|
+
kind: ConfigMap
|
|
1605
|
+
metadata:
|
|
1606
|
+
name: ${alertmanager.configMapName}
|
|
1607
|
+
namespace: ${namespace}
|
|
1608
|
+
labels:
|
|
1609
|
+
app: ${alertmanager.name}
|
|
1610
|
+
data:
|
|
1611
|
+
alertmanager.yml: |
|
|
1612
|
+
${indentBlock(alertmanagerConf, 4)}
|
|
1613
|
+
---
|
|
1614
|
+
apiVersion: v1
|
|
1615
|
+
kind: ConfigMap
|
|
1616
|
+
metadata:
|
|
1617
|
+
name: ${blackbox.configMapName}
|
|
1618
|
+
namespace: ${namespace}
|
|
1619
|
+
labels:
|
|
1620
|
+
app: ${blackbox.name}
|
|
1621
|
+
data:
|
|
1622
|
+
blackbox.yml: |
|
|
1623
|
+
${indentBlock(blackboxConf, 4)}
|
|
1624
|
+
---
|
|
1625
|
+
apiVersion: v1
|
|
1626
|
+
kind: ConfigMap
|
|
1627
|
+
metadata:
|
|
1628
|
+
name: ${grafana.datasourcesConfigMapName}
|
|
1629
|
+
namespace: ${namespace}
|
|
1630
|
+
labels:
|
|
1631
|
+
app: ${grafana.name}
|
|
1632
|
+
data:
|
|
1633
|
+
underpost.yaml: |
|
|
1634
|
+
${indentBlock(grafanaDatasourcesFactory({ namespace }), 4)}
|
|
1635
|
+
---
|
|
1636
|
+
apiVersion: v1
|
|
1637
|
+
kind: ConfigMap
|
|
1638
|
+
metadata:
|
|
1639
|
+
name: ${grafana.dashboardProviderConfigMapName}
|
|
1640
|
+
namespace: ${namespace}
|
|
1641
|
+
labels:
|
|
1642
|
+
app: ${grafana.name}
|
|
1643
|
+
data:
|
|
1644
|
+
underpost.yaml: |
|
|
1645
|
+
${indentBlock(grafanaDashboardProviderFactory(), 4)}
|
|
1646
|
+
---
|
|
1647
|
+
apiVersion: v1
|
|
1648
|
+
kind: ConfigMap
|
|
1649
|
+
metadata:
|
|
1650
|
+
name: ${grafana.dashboardsConfigMapName}
|
|
1651
|
+
namespace: ${namespace}
|
|
1652
|
+
labels:
|
|
1653
|
+
app: ${grafana.name}
|
|
1654
|
+
data:
|
|
1655
|
+
underpost-envoy.json: |
|
|
1656
|
+
${indentBlock(envoyDashboardFactory(), 4)}
|
|
1657
|
+
underpost-events.json: |
|
|
1658
|
+
${indentBlock(eventsDashboardFactory(), 4)}
|
|
1659
|
+
underpost-node-metrics.json: |
|
|
1660
|
+
${indentBlock(nodeMetricsDashboardFactory(), 4)}
|
|
1661
|
+
`;
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
/**
|
|
1665
|
+
* @method webhookSecretFactory
|
|
1666
|
+
* @description Renders the Secret holding the bearer token Alertmanager
|
|
1667
|
+
* presents to the dispatcher.
|
|
1668
|
+
*
|
|
1669
|
+
* A Secret rather than a field in the Alertmanager ConfigMap: the same token
|
|
1670
|
+
* authorizes anything that can reach the receiver to trigger remediation on the
|
|
1671
|
+
* hub, so it must not sit in a document `kubectl get -o yaml` prints in full.
|
|
1672
|
+
* @param {object} params
|
|
1673
|
+
* @param {string} params.token - Shared bearer token.
|
|
1674
|
+
* @param {string} [params.namespace] - Namespace to render into.
|
|
1675
|
+
* @returns {string} Secret YAML.
|
|
1676
|
+
* @memberof UnderpostMonitoring
|
|
1677
|
+
*/
|
|
1678
|
+
const webhookSecretFactory = ({ token, namespace = 'default' } = {}) => {
|
|
1679
|
+
const { alertmanager } = UNDERPOST_MONITORING;
|
|
1680
|
+
return `
|
|
1681
|
+
---
|
|
1682
|
+
apiVersion: v1
|
|
1683
|
+
kind: Secret
|
|
1684
|
+
metadata:
|
|
1685
|
+
name: ${alertmanager.secretName}
|
|
1686
|
+
namespace: ${namespace}
|
|
1687
|
+
labels:
|
|
1688
|
+
app: ${alertmanager.name}
|
|
1689
|
+
type: Opaque
|
|
1690
|
+
data:
|
|
1691
|
+
${alertmanager.tokenKey}: ${Buffer.from(`${token}`, 'utf8').toString('base64')}
|
|
1692
|
+
`;
|
|
1693
|
+
};
|
|
1694
|
+
|
|
1695
|
+
export {
|
|
1696
|
+
UNDERPOST_MONITORING,
|
|
1697
|
+
alertRulesFactory,
|
|
1698
|
+
alertmanagerConfFactory,
|
|
1699
|
+
appScrapeEntriesFactory,
|
|
1700
|
+
blackboxConfFactory,
|
|
1701
|
+
deployedEventIdsFactory,
|
|
1702
|
+
envoyDashboardFactory,
|
|
1703
|
+
eventsDashboardFactory,
|
|
1704
|
+
nodeExporterManifestFactory,
|
|
1705
|
+
nodeExporterServiceFactory,
|
|
1706
|
+
nodeExporterServiceScriptFactory,
|
|
1707
|
+
nodeMetricsDashboardFactory,
|
|
1708
|
+
grafanaAdminSecretFactory,
|
|
1709
|
+
grafanaDeploymentFactory,
|
|
1710
|
+
grafanaDeploymentPatchFactory,
|
|
1711
|
+
grafanaDashboardProviderFactory,
|
|
1712
|
+
grafanaExposureFactory,
|
|
1713
|
+
grafanaNodePortFactory,
|
|
1714
|
+
grafanaResetPlanFactory,
|
|
1715
|
+
grafanaStorageResetRequiredFactory,
|
|
1716
|
+
grafanaDatasourcesFactory,
|
|
1717
|
+
metricsPathFactory,
|
|
1718
|
+
monitoringConfigFactory,
|
|
1719
|
+
probeGroupsFactory,
|
|
1720
|
+
prometheusConfFactory,
|
|
1721
|
+
scrapeDeployListFactory,
|
|
1722
|
+
serviceDnsFactory,
|
|
1723
|
+
webhookSecretFactory,
|
|
1724
|
+
};
|