@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,949 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UnderpostCron server module
|
|
3
|
+
*
|
|
4
|
+
* ## Host mounts and SELinux
|
|
5
|
+
*
|
|
6
|
+
* Generated CronJob pods run as `container_t`, so every host path they mount has to carry a type
|
|
7
|
+
* that domain can use, and no host path carries a credential:
|
|
8
|
+
*
|
|
9
|
+
* - **the global underpost directory** (`<npm root>/underpost`, i.e. root's nvm tree) was mounted
|
|
10
|
+
* over the image's own copy of the CLI purely so the container could read `.env` and `.state`
|
|
11
|
+
* from it. It carried `admin_home_t`, which `container_t` can neither read nor write, and it
|
|
12
|
+
* exposed the whole of root's npm tree to get at two variables. Replaced by an optional
|
|
13
|
+
* `envFrom` Secret carrying exactly those values.
|
|
14
|
+
* - **the node-exporter textfile directory** is written by the job bodies. Created by the
|
|
15
|
+
* collector's installer under `/var/lib`, so it inherits a type `container_t` may read but not
|
|
16
|
+
* write; {@link prepareCronHostStorage} gives it the shared container label.
|
|
17
|
+
* - **the engine mirror** at {@link ENGINE_MIRROR_PATH}. The job body is `node bin ...` — it
|
|
18
|
+
* executes *this repository*, so it needs the source tree, its `node_modules` and the deploy
|
|
19
|
+
* configuration it resolves — {@link engineMirrorContentsFactory} is that set, and nothing wider.
|
|
20
|
+
* The checkout itself is never mounted. It lives in the operator's home tree, where
|
|
21
|
+
* `container_t` is denied every read, and the fix cannot be to relabel it: it is a live working
|
|
22
|
+
* tree, and the shared container label on it would make everything ever written there
|
|
23
|
+
* container-readable. So the pods mount a dedicated copy outside `/home` instead, refreshed
|
|
24
|
+
* from the checkout and labeled by {@link prepareCronHostStorage}. The mount lands on the
|
|
25
|
+
* checkout's own path inside the container, so nothing the job body runs knows the difference.
|
|
26
|
+
* - **the connection key**, a projected Secret rather than a host path. `ssh` authenticates with a
|
|
27
|
+
* file, so it cannot travel in the `envFrom` Secret beside the tokens; and it must not travel in
|
|
28
|
+
* the mirror, which every `container_t` process on the node can read.
|
|
29
|
+
* {@link UnderpostSSH.keyPathFactory} prefers the mount, so a pod and the host CLI take the same
|
|
30
|
+
* code path to different keys.
|
|
31
|
+
*
|
|
32
|
+
* @module src/server/ops/cron.js
|
|
33
|
+
* @namespace UnderpostCron
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { loggerFactory } from './logger.js';
|
|
37
|
+
import { shellArgumentFactory, shellExec } from '../runtime/process.js';
|
|
38
|
+
import fs from 'fs-extra';
|
|
39
|
+
import os from 'node:os';
|
|
40
|
+
import dotenv from 'dotenv';
|
|
41
|
+
import Underpost from '../../index.js';
|
|
42
|
+
import { DEPLOY_ROUTES_PATH, readDeployRoutes } from '../network/router.js';
|
|
43
|
+
import { UNDERPOST_MONITORING } from './monitoring.js';
|
|
44
|
+
import ContainerStorageService, { ensureContainerStorage } from '../security/container-storage.js';
|
|
45
|
+
import { assertRoleCapability } from '../network/node-capability.js';
|
|
46
|
+
|
|
47
|
+
const logger = loggerFactory(import.meta);
|
|
48
|
+
|
|
49
|
+
// Where the job body runs, inside the container. The image is built around this path and every
|
|
50
|
+
// relative path the CLI resolves hangs off it, so the mount lands here whatever its host source.
|
|
51
|
+
const enginePath = '/home/dd/engine';
|
|
52
|
+
// The host source that mount reads: a copy of the checkout outside every home tree, which is the
|
|
53
|
+
// only way an unprivileged pod can read this platform's own source. See the module header.
|
|
54
|
+
const ENGINE_MIRROR_PATH = '/opt/engine';
|
|
55
|
+
const cronVolumeName = 'underpost-cron-container-volume';
|
|
56
|
+
const textfileVolumeName = 'underpost-node-exporter-textfile';
|
|
57
|
+
// The credentials the job bodies read out of the environment (`GITHUB_TOKEN`, `GITHUB_USERNAME`).
|
|
58
|
+
// They used to arrive by bind-mounting the operator's global underpost directory out of root's
|
|
59
|
+
// home over the image's own copy of the CLI; a Secret delivers exactly those values, is labeled
|
|
60
|
+
// for container consumption by kubelet, and exposes nothing else of that tree.
|
|
61
|
+
const cronEnvSecretName = 'underpost-cron-env';
|
|
62
|
+
// The connection key, projected as a volume: ssh authenticates with a file, and the mirror the
|
|
63
|
+
// pods mount deliberately carries no key material. `UnderpostSSH.keyPathFactory` reads it here.
|
|
64
|
+
const cronSshSecretName = 'underpost-ssh-key';
|
|
65
|
+
const sshSecretMountPath = '/etc/underpost/secrets/ssh';
|
|
66
|
+
const sshVolumeName = 'underpost-ssh-key';
|
|
67
|
+
const DEFAULT_CRON_ID = 'dd-cron';
|
|
68
|
+
|
|
69
|
+
/** The environment a generated pod runs. Rendered into the manifest, where it wins over the
|
|
70
|
+
* projected Secret, so the resolved value and the running value cannot drift. */
|
|
71
|
+
const cronDeployEnvFactory = (dev) => (dev === true ? 'development' : 'production');
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolves the deploy ID stored in `engine-private/deploy/dd.cron`.
|
|
75
|
+
* @returns {string|null}
|
|
76
|
+
* @memberof UnderpostCron
|
|
77
|
+
*/
|
|
78
|
+
const cronDeployIdResolve = () => {
|
|
79
|
+
const path = './engine-private/deploy/dd.cron';
|
|
80
|
+
if (!fs.existsSync(path)) return null;
|
|
81
|
+
return fs.readFileSync(path, 'utf8').trim() || null;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Loads cron and router deployment environment files into `process.env`.
|
|
86
|
+
* @returns {void}
|
|
87
|
+
* @memberof UnderpostCron
|
|
88
|
+
*/
|
|
89
|
+
const loadCronDeployEnv = () => {
|
|
90
|
+
const envName = process.env.NODE_ENV || 'production';
|
|
91
|
+
const cronDeployId = cronDeployIdResolve();
|
|
92
|
+
|
|
93
|
+
if (cronDeployId) {
|
|
94
|
+
const path = `./engine-private/conf/${cronDeployId}/.env.${envName}`;
|
|
95
|
+
if (fs.existsSync(path)) process.env = { ...process.env, ...dotenv.parse(fs.readFileSync(path, 'utf8')) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const id of readDeployRoutes()) {
|
|
99
|
+
const path = `./engine-private/conf/${id}/.env.${envName}`;
|
|
100
|
+
if (!id || !fs.existsSync(path)) continue;
|
|
101
|
+
const env = dotenv.parse(fs.readFileSync(path, 'utf8'));
|
|
102
|
+
for (const [key, value] of Object.entries(env)) {
|
|
103
|
+
if (!(key in process.env)) process.env[key] = value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Generates a Kubernetes CronJob YAML manifest string.
|
|
110
|
+
*
|
|
111
|
+
* @param {Object} params - CronJob parameters
|
|
112
|
+
* @param {string} params.name - CronJob name (max 52 chars, sanitized to DNS subdomain)
|
|
113
|
+
* @param {string} params.expression - Cron schedule expression (e.g., '0 0 * * *')
|
|
114
|
+
* @param {string} params.deployList - Comma-separated deploy IDs for the cron CLI
|
|
115
|
+
* @param {string} params.jobList - Comma-separated job IDs (e.g., 'dns', 'backup')
|
|
116
|
+
* @param {string} [params.image] - Container image (defaults to underpost/underpost-engine:<version>)
|
|
117
|
+
* @param {string} [params.namespace='default'] - Kubernetes namespace
|
|
118
|
+
* @param {boolean} [params.git=false] - Pass --git flag to cron CLI
|
|
119
|
+
* @param {boolean} [params.dev=false] - Use local ./ base path instead of global underpost installation
|
|
120
|
+
* @param {string} [params.cmd] - Optional pre-script commands to run before cron execution
|
|
121
|
+
* @param {boolean} [params.suspend=false] - Whether the CronJob is suspended
|
|
122
|
+
* @param {boolean} [params.dryRun=false] - Pass --dry-run flag to the cron command inside the container
|
|
123
|
+
* @param {boolean} [params.k3s=false] - Pass --k3s flag to the cron command inside the container
|
|
124
|
+
* @param {boolean} [params.kind=false] - Pass --kind flag to the cron command inside the container
|
|
125
|
+
* @param {boolean} [params.kubeadm=false] - Pass --kubeadm flag to the cron command inside the container
|
|
126
|
+
* @param {string} [params.nodeName] - Pin the Job pod to this node via a `kubernetes.io/hostname` nodeSelector.
|
|
127
|
+
* Placement is a manifest concern only, so it is never forwarded to the cron command inside the container.
|
|
128
|
+
* @returns {string} Kubernetes CronJob YAML manifest
|
|
129
|
+
* @memberof UnderpostCron
|
|
130
|
+
*/
|
|
131
|
+
const cronJobYamlFactory = ({
|
|
132
|
+
name,
|
|
133
|
+
expression,
|
|
134
|
+
deployList,
|
|
135
|
+
jobList,
|
|
136
|
+
image,
|
|
137
|
+
namespace = 'default',
|
|
138
|
+
git = false,
|
|
139
|
+
dev = false,
|
|
140
|
+
cmd,
|
|
141
|
+
suspend = false,
|
|
142
|
+
dryRun = false,
|
|
143
|
+
k3s = false,
|
|
144
|
+
kind = false,
|
|
145
|
+
kubeadm = false,
|
|
146
|
+
nodeName = '',
|
|
147
|
+
}) => {
|
|
148
|
+
const containerImage = image || `underpost/underpost-engine:${Underpost.version}`;
|
|
149
|
+
|
|
150
|
+
const sanitizedName = name
|
|
151
|
+
.toLowerCase()
|
|
152
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
153
|
+
.replace(/--+/g, '-')
|
|
154
|
+
.replace(/^-|-$/g, '')
|
|
155
|
+
.substring(0, 52);
|
|
156
|
+
|
|
157
|
+
const cronDeployId = cronDeployIdResolve();
|
|
158
|
+
|
|
159
|
+
const flags = `${git ? '--git ' : ''}${dev ? '--dev ' : ''}${dryRun ? '--dry-run ' : ''}${k3s ? '--k3s ' : ''}${kind ? '--kind ' : ''}${kubeadm ? '--kubeadm ' : ''}`;
|
|
160
|
+
// No `app load`: it materializes a deployment's environment from its env file, and the pod has
|
|
161
|
+
// neither. Its environment arrives injected, scoped to what the jobs consume; the `conf.*.json`
|
|
162
|
+
// the jobs read carry `env:` references that resolve against exactly that.
|
|
163
|
+
const commands = [`cd ${enginePath}`];
|
|
164
|
+
if (cmd) commands.push(cmd);
|
|
165
|
+
commands.push(`node bin cron ${deployList} ${jobList} ${flags}`);
|
|
166
|
+
const fullCommand = commands.join(' &&\n ');
|
|
167
|
+
|
|
168
|
+
return `apiVersion: batch/v1
|
|
169
|
+
kind: CronJob
|
|
170
|
+
metadata:
|
|
171
|
+
name: ${sanitizedName}
|
|
172
|
+
namespace: ${namespace}
|
|
173
|
+
labels:
|
|
174
|
+
app: ${sanitizedName}
|
|
175
|
+
managed-by: underpost
|
|
176
|
+
spec:
|
|
177
|
+
schedule: "${expression}"
|
|
178
|
+
concurrencyPolicy: Forbid
|
|
179
|
+
startingDeadlineSeconds: 200
|
|
180
|
+
successfulJobsHistoryLimit: 3
|
|
181
|
+
failedJobsHistoryLimit: 1
|
|
182
|
+
suspend: ${suspend}
|
|
183
|
+
jobTemplate:
|
|
184
|
+
metadata:
|
|
185
|
+
labels:
|
|
186
|
+
app: ${sanitizedName}
|
|
187
|
+
managed-by: underpost
|
|
188
|
+
spec:
|
|
189
|
+
template:
|
|
190
|
+
metadata:
|
|
191
|
+
labels:
|
|
192
|
+
app: ${sanitizedName}
|
|
193
|
+
managed-by: underpost
|
|
194
|
+
spec:
|
|
195
|
+
${
|
|
196
|
+
nodeName
|
|
197
|
+
? ` nodeSelector:
|
|
198
|
+
kubernetes.io/hostname: ${nodeName}
|
|
199
|
+
`
|
|
200
|
+
: ''
|
|
201
|
+
} containers:
|
|
202
|
+
- name: ${sanitizedName}
|
|
203
|
+
image: ${containerImage}
|
|
204
|
+
command:
|
|
205
|
+
- /bin/sh
|
|
206
|
+
- -c
|
|
207
|
+
- >
|
|
208
|
+
${fullCommand}
|
|
209
|
+
# Explicit, so the environment the pod runs is the one this manifest resolved:
|
|
210
|
+
# a container env entry wins over envFrom, whatever the projected Secret carries.
|
|
211
|
+
env:
|
|
212
|
+
- name: NODE_ENV
|
|
213
|
+
value: ${cronDeployEnvFactory(dev)}
|
|
214
|
+
envFrom:
|
|
215
|
+
- secretRef:
|
|
216
|
+
name: ${cronEnvSecretName}
|
|
217
|
+
optional: true
|
|
218
|
+
volumeMounts:
|
|
219
|
+
- mountPath: ${enginePath}
|
|
220
|
+
name: ${cronVolumeName}
|
|
221
|
+
- mountPath: ${UNDERPOST_MONITORING.nodeExporter.textfileDirectory}
|
|
222
|
+
name: ${textfileVolumeName}
|
|
223
|
+
- mountPath: ${sshSecretMountPath}
|
|
224
|
+
name: ${sshVolumeName}
|
|
225
|
+
readOnly: true
|
|
226
|
+
volumes:
|
|
227
|
+
# The engine mirror: a copy of the checkout kept outside every home tree, because
|
|
228
|
+
# \`container_t\` is denied every read under one. The job body runs this repository's CLI
|
|
229
|
+
# (\`node bin ...\`), so it needs the source tree, its node_modules and engine-private's
|
|
230
|
+
# deploy configuration — the mount cannot be narrowed below that. Refreshed and labeled
|
|
231
|
+
# by prepareCronHostStorage(); see src/server/ops/cron.js's module header.
|
|
232
|
+
- hostPath:
|
|
233
|
+
path: ${ENGINE_MIRROR_PATH}
|
|
234
|
+
type: Directory
|
|
235
|
+
name: ${cronVolumeName}
|
|
236
|
+
# Metrics the cluster cannot scrape for itself are written here and read by the
|
|
237
|
+
# node-exporter collector. Prepared and labeled by prepareCronHostStorage().
|
|
238
|
+
- hostPath:
|
|
239
|
+
path: ${UNDERPOST_MONITORING.nodeExporter.textfileDirectory}
|
|
240
|
+
type: DirectoryOrCreate
|
|
241
|
+
name: ${textfileVolumeName}
|
|
242
|
+
# Optional for the same reason the credential Secret is: a cluster that has onboarded
|
|
243
|
+
# no key still schedules, and the job that needs one reports its own failure.
|
|
244
|
+
- secret:
|
|
245
|
+
secretName: ${cronSshSecretName}
|
|
246
|
+
optional: true
|
|
247
|
+
defaultMode: 0400
|
|
248
|
+
name: ${sshVolumeName}
|
|
249
|
+
restartPolicy: OnFailure
|
|
250
|
+
`;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Prepares the node directories the generated CronJobs mount, and gives the one they write to a
|
|
255
|
+
* label an unprivileged container can actually use.
|
|
256
|
+
*
|
|
257
|
+
* The node-exporter textfile directory is created by the collector's own installer under
|
|
258
|
+
* `/var/lib`, so it inherits that tree's policy type — which `container_t` may read but never
|
|
259
|
+
* write. The cron pods write their metrics there, so without this the first Enforcing run turns
|
|
260
|
+
* every `*.prom` write into a denial.
|
|
261
|
+
*
|
|
262
|
+
* The engine mirror is refreshed from the checkout first, then labeled with the rest: the pods
|
|
263
|
+
* execute it, so a stale or unlabeled mirror is the same outage. The checkout itself is never
|
|
264
|
+
* prepared — it is the operator's working tree, and the shared container label on it would make
|
|
265
|
+
* everything written there container-readable.
|
|
266
|
+
* @returns {string[]} Prepared paths.
|
|
267
|
+
* @memberof UnderpostCron
|
|
268
|
+
*/
|
|
269
|
+
const prepareCronHostStorage = () => {
|
|
270
|
+
syncEngineMirror();
|
|
271
|
+
|
|
272
|
+
const paths = [UNDERPOST_MONITORING.nodeExporter.textfileDirectory, ENGINE_MIRROR_PATH];
|
|
273
|
+
ensureContainerStorage(paths, { execute: shellExec });
|
|
274
|
+
|
|
275
|
+
const mirror = cronCheckoutContextFactory();
|
|
276
|
+
if (!mirror.readable)
|
|
277
|
+
logger.warn('CronJob pods cannot read the engine mirror they mount', {
|
|
278
|
+
path: ENGINE_MIRROR_PATH,
|
|
279
|
+
type: mirror.type,
|
|
280
|
+
effect: `every job body fails with Cannot find module '${enginePath}/bin'`,
|
|
281
|
+
remedy: `label it ${ContainerStorageService.SHARED_CONTAINER_TYPE}: sudo restorecon -RF ${ENGINE_MIRROR_PATH}`,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return paths;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Syncs the engine directory into the kind-worker container node.
|
|
289
|
+
* Required for kind clusters where worker nodes don't share the host filesystem.
|
|
290
|
+
*
|
|
291
|
+
* @memberof UnderpostCron
|
|
292
|
+
*/
|
|
293
|
+
const syncEngineToKindWorker = () => {
|
|
294
|
+
logger.info('Syncing engine mirror to kind-worker node', { path: ENGINE_MIRROR_PATH });
|
|
295
|
+
shellExec(`docker exec -i kind-worker bash -c "rm -rf ${ENGINE_MIRROR_PATH}"`);
|
|
296
|
+
shellExec(`docker cp ${ENGINE_MIRROR_PATH} kind-worker:${ENGINE_MIRROR_PATH}`);
|
|
297
|
+
shellExec(
|
|
298
|
+
`docker exec -i kind-worker bash -c "chown -R 1000:1000 ${ENGINE_MIRROR_PATH}; chmod -R 755 ${ENGINE_MIRROR_PATH}"`,
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Resolves the deploy-id to use for cron job generation.
|
|
304
|
+
* Uses the explicit value or the deploy ID stored in `dd.cron`.
|
|
305
|
+
*
|
|
306
|
+
* @param {string} [deployId] - Explicit deploy-id override
|
|
307
|
+
* @memberof UnderpostCron
|
|
308
|
+
* @returns {string|null} Resolved deploy-id or null if not found
|
|
309
|
+
*/
|
|
310
|
+
const resolveDeployId = (deployId) => (deployId && deployId !== 'dd' ? deployId : cronDeployIdResolve());
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Parses a comma-separated CLI list into a trimmed, non-empty array.
|
|
314
|
+
*
|
|
315
|
+
* @param {string|string[]} [value] - Raw CLI value
|
|
316
|
+
* @memberof UnderpostCron
|
|
317
|
+
* @returns {string[]} Parsed entries
|
|
318
|
+
*/
|
|
319
|
+
const parseList = (value) => {
|
|
320
|
+
if (Array.isArray(value)) return value.map((entry) => `${entry}`.trim()).filter(Boolean);
|
|
321
|
+
if (typeof value !== 'string') return [];
|
|
322
|
+
return value
|
|
323
|
+
.split(',')
|
|
324
|
+
.map((entry) => entry.trim())
|
|
325
|
+
.filter(Boolean);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Resolves the deploy-id list a job callback receives.
|
|
330
|
+
*
|
|
331
|
+
* Always a comma-separated string: every job callback splits it on `,`, and
|
|
332
|
+
* `getRelatedDeployIdList` already returns that shape, so an explicit list is
|
|
333
|
+
* normalized to match rather than handed over as a parsed array.
|
|
334
|
+
*
|
|
335
|
+
* @param {string} jobId - Job identifier, used to pick the default source file
|
|
336
|
+
* @param {string|string[]} [deployList] - Explicit deploy-list CLI value
|
|
337
|
+
* @memberof UnderpostCron
|
|
338
|
+
* @returns {string} Comma-separated deploy IDs
|
|
339
|
+
*/
|
|
340
|
+
const resolveJobDeployList = (jobId, deployList) =>
|
|
341
|
+
!deployList || deployList === 'dd' ? Underpost.cron.getRelatedDeployIdList(jobId) : parseList(deployList).join(',');
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Checks whether a node is registered on the cluster.
|
|
345
|
+
*
|
|
346
|
+
* @param {string} nodeName - Node name
|
|
347
|
+
* @memberof UnderpostCron
|
|
348
|
+
* @returns {boolean} True when the node exists
|
|
349
|
+
*/
|
|
350
|
+
const nodeExists = (nodeName) => {
|
|
351
|
+
const stdout = shellExec(`kubectl get node ${nodeName} -o name`, {
|
|
352
|
+
silent: true,
|
|
353
|
+
stdout: true,
|
|
354
|
+
silentOnError: true,
|
|
355
|
+
disableLog: true,
|
|
356
|
+
});
|
|
357
|
+
return `${stdout || ''}`.trim().length > 0;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Whether the pods can read the mirror they mount, and the type denying them when they cannot.
|
|
362
|
+
*
|
|
363
|
+
* The job body is the engine's own CLI, so a mount `container_t` may not read surfaces as
|
|
364
|
+
* `Cannot find module '<engine>/bin'` — a Node error naming neither the mount nor the policy that
|
|
365
|
+
* denied it. {@link prepareCronHostStorage} labels the mirror, so this is the post-condition on
|
|
366
|
+
* that: a mirror that still denies means the labeling did not take, not that the mount is wrong.
|
|
367
|
+
* Off an Enforcing host nothing denies, so nothing is claimed.
|
|
368
|
+
*
|
|
369
|
+
* @memberof UnderpostCron
|
|
370
|
+
* @returns {{readable: boolean, type: string}} Whether a pod can read the mount, and its SELinux type.
|
|
371
|
+
*/
|
|
372
|
+
const cronCheckoutContextFactory = () => {
|
|
373
|
+
const read = (command) =>
|
|
374
|
+
`${shellExec(command, { stdout: true, silent: true, silentOnError: true, disableLog: true }) || ''}`.trim();
|
|
375
|
+
|
|
376
|
+
if (read('command -v getenforce >/dev/null 2>&1 && getenforce') !== 'Enforcing') return { readable: true, type: '' };
|
|
377
|
+
|
|
378
|
+
const type = (read(`ls -Zd ${ENGINE_MIRROR_PATH}`).split(/\s+/)[0] || '').split(':')[2] || '';
|
|
379
|
+
return { readable: type === ContainerStorageService.SHARED_CONTAINER_TYPE, type };
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* @method engineMirrorContentsFactory
|
|
384
|
+
* @description Exactly what a job body resolves, as rsync filter rules over the checkout.
|
|
385
|
+
*
|
|
386
|
+
* An allowlist, not a denylist, for two reasons that both bite in production. The mirror carries
|
|
387
|
+
* the shared container label, so every `container_t` process on the node can read it: a denylist
|
|
388
|
+
* leaks whatever the repository gains next, and the thing it gains may be a key. And the mirror is
|
|
389
|
+
* copied on every apply, so a denylist grows without anyone deciding to grow it.
|
|
390
|
+
*
|
|
391
|
+
* `engine-private` is narrowed rather than dropped — the pod cannot run without it. `app load`
|
|
392
|
+
* reads `conf/<deploy-id>/.env.<env>` and that deploy's `conf.*.json`; `cron` reads
|
|
393
|
+
* `deploy/dd.cron`, `deploy/dd.routes` and the env of every routed deploy. That is the whole set.
|
|
394
|
+
* No key material is mirrored: the connection key arrives as a projected Secret volume.
|
|
395
|
+
*
|
|
396
|
+
* `node_modules` is present and `.git` is not, which is why the repository's own ignore files
|
|
397
|
+
* cannot express this set: they describe what is not source, and this is what is executed.
|
|
398
|
+
*
|
|
399
|
+
* No `.env.*` at any environment. Each one is a deployment's entire credential set, and the
|
|
400
|
+
* mirror is readable by every `container_t` process on the node; the pods receive the keys their
|
|
401
|
+
* scope entitles them to as an injected environment instead. `conf.*.json` stay because they hold
|
|
402
|
+
* `env:` references rather than values.
|
|
403
|
+
* @returns {string[]} rsync filter rules, in precedence order.
|
|
404
|
+
* @memberof UnderpostCron
|
|
405
|
+
*/
|
|
406
|
+
const engineMirrorContentsFactory = () => [
|
|
407
|
+
// rsync takes the first rule that matches, so the asset trees are subtracted before `/src/**`
|
|
408
|
+
// adds their parents back. They are data the CLI never opens, and the bulk of the checkout.
|
|
409
|
+
'--exclude=/src/client/public/**',
|
|
410
|
+
// The CLI, its modules, and the manifest that makes them resolvable. `/src` is taken whole
|
|
411
|
+
// apart from those two subtrees: it is one import graph, and the entrypoint pulls from every
|
|
412
|
+
// corner of it — `src/runtime/nginx` among them.
|
|
413
|
+
'--include=/bin/',
|
|
414
|
+
'--include=/bin/**',
|
|
415
|
+
'--include=/src/',
|
|
416
|
+
'--include=/src/**',
|
|
417
|
+
'--include=/package.json',
|
|
418
|
+
'--include=/conf.js',
|
|
419
|
+
'--include=/node_modules/',
|
|
420
|
+
'--include=/node_modules/**',
|
|
421
|
+
// The deploy configuration the job bodies resolve, named file-shape by file-shape rather than
|
|
422
|
+
// globbed: `conf/<id>` also holds per-deploy key pairs and `deploy/` holds the fleet's own, and
|
|
423
|
+
// an allowlist is only worth having if it stops at the files that are actually read.
|
|
424
|
+
'--include=/engine-private/',
|
|
425
|
+
'--include=/engine-private/conf/',
|
|
426
|
+
'--include=/engine-private/conf/*/',
|
|
427
|
+
'--include=/engine-private/conf/*/conf.*.json',
|
|
428
|
+
'--include=/engine-private/conf/*/package.json',
|
|
429
|
+
'--include=/engine-private/deploy/',
|
|
430
|
+
'--include=/engine-private/deploy/dd.cron',
|
|
431
|
+
'--include=/engine-private/deploy/dd.routes',
|
|
432
|
+
'--exclude=*',
|
|
433
|
+
];
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Refreshes the engine mirror the CronJob pods mount from the checkout this command runs in.
|
|
437
|
+
*
|
|
438
|
+
* `--delete` because the mirror is an output, not a second working tree: a file the checkout no
|
|
439
|
+
* longer has must not survive in the tree the pods execute.
|
|
440
|
+
*
|
|
441
|
+
* @memberof UnderpostCron
|
|
442
|
+
* @returns {{source: string, target: string}} What was mirrored, and where.
|
|
443
|
+
*/
|
|
444
|
+
const syncEngineMirror = () => {
|
|
445
|
+
const source = process.cwd();
|
|
446
|
+
const filters = engineMirrorContentsFactory().map(shellArgumentFactory).join(' ');
|
|
447
|
+
|
|
448
|
+
logger.info('Refreshing the engine mirror the CronJob pods mount', { source, target: ENGINE_MIRROR_PATH });
|
|
449
|
+
shellExec(
|
|
450
|
+
`sudo rsync -a --delete ${filters} ${shellArgumentFactory(`${source}/`)} ${shellArgumentFactory(`${ENGINE_MIRROR_PATH}/`)}`,
|
|
451
|
+
);
|
|
452
|
+
return { source, target: ENGINE_MIRROR_PATH };
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Resolves the node a generated CronJob's pods are pinned to.
|
|
457
|
+
*
|
|
458
|
+
* Placement is not optional for these manifests. The job body runs the engine checkout they
|
|
459
|
+
* hostPath-mount, and a hostPath is node-local: an unpinned pod is free to land on a node whose
|
|
460
|
+
* `/home/dd/engine` is some other directory of that name, where the body dies on
|
|
461
|
+
* `Cannot find module '/home/dd/engine/bin'` before it can report anything of its own. The node
|
|
462
|
+
* generating the manifest is the one holding the checkout it mounts, so it is the default —
|
|
463
|
+
* claimed only when the cluster actually knows it under that name, so a workstation that is not
|
|
464
|
+
* a node keeps producing the portable manifest it produced before.
|
|
465
|
+
*
|
|
466
|
+
* @param {string} [nodeName] - Explicit `--node-name`, which always wins.
|
|
467
|
+
* @memberof UnderpostCron
|
|
468
|
+
* @returns {string} Node to pin to, or `''` to leave the pods unpinned.
|
|
469
|
+
*/
|
|
470
|
+
const cronNodeNameFactory = (nodeName) => {
|
|
471
|
+
const requested = `${nodeName || ''}`.trim();
|
|
472
|
+
if (requested) return requested;
|
|
473
|
+
|
|
474
|
+
const local = os.hostname();
|
|
475
|
+
if (!local || !nodeExists(local)) return '';
|
|
476
|
+
logger.info('Pinning CronJob pods to the node holding the mounted checkout', { nodeName: local });
|
|
477
|
+
return local;
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Resolves the cluster context a manifest run applies under.
|
|
482
|
+
*
|
|
483
|
+
* The context is not decoration: it selects how the image reaches the cluster and is rendered
|
|
484
|
+
* into the pod's own command, so a run that names none published a CronJob whose body addressed
|
|
485
|
+
* no cluster at all. A node can be asked which runtime it belongs to, so an unnamed context is
|
|
486
|
+
* read from the cluster rather than left blank.
|
|
487
|
+
*
|
|
488
|
+
* @param {object} [options] - CLI options carrying `k3s`, `kind` or `kubeadm`.
|
|
489
|
+
* @memberof UnderpostCron
|
|
490
|
+
* @returns {{k3s: boolean, kind: boolean, kubeadm: boolean}} Mutually exclusive context flags.
|
|
491
|
+
*/
|
|
492
|
+
const cronClusterContextFactory = (options = {}) => {
|
|
493
|
+
if (options.k3s || options.kind || options.kubeadm)
|
|
494
|
+
return { k3s: !!options.k3s, kind: !!options.kind, kubeadm: !!options.kubeadm };
|
|
495
|
+
|
|
496
|
+
const { type } = Underpost.cluster.detectClusterRuntime();
|
|
497
|
+
if (type) logger.info('Resolved the cluster context from the cluster itself', { clusterType: type });
|
|
498
|
+
return { k3s: type === 'k3s', kind: type === 'kind', kubeadm: type === 'kubeadm' };
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Checks whether a CronJob is already published on the cluster.
|
|
503
|
+
*
|
|
504
|
+
* @param {string} cronJobName - Sanitized CronJob name
|
|
505
|
+
* @param {string} namespace - Kubernetes namespace
|
|
506
|
+
* @memberof UnderpostCron
|
|
507
|
+
* @returns {boolean} True when the CronJob exists
|
|
508
|
+
*/
|
|
509
|
+
const cronJobExists = (cronJobName, namespace) => {
|
|
510
|
+
const stdout = shellExec(`kubectl get cronjob ${cronJobName} -n ${namespace} --ignore-not-found -o name`, {
|
|
511
|
+
silent: true,
|
|
512
|
+
stdout: true,
|
|
513
|
+
silentOnError: true,
|
|
514
|
+
disableLog: true,
|
|
515
|
+
});
|
|
516
|
+
return `${stdout || ''}`.trim().length > 0;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* UnderpostCron main module methods
|
|
521
|
+
* @class UnderpostCron
|
|
522
|
+
* @memberof UnderpostCron
|
|
523
|
+
*/
|
|
524
|
+
class UnderpostCron {
|
|
525
|
+
/** @returns {Object} Available cron job handlers */
|
|
526
|
+
static get JOB() {
|
|
527
|
+
return {
|
|
528
|
+
dns: Underpost.dns,
|
|
529
|
+
backup: Underpost.backup,
|
|
530
|
+
vultr: Underpost.vultr,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
static API = {
|
|
535
|
+
/**
|
|
536
|
+
* CLI entry point for the `underpost cron` command.
|
|
537
|
+
*
|
|
538
|
+
* Manifest modes (`--setup-start`, `--generate-k8s-cronjobs`, `--apply`, `--create-job-now`)
|
|
539
|
+
* never run job callbacks in this process: they write and publish manifests, and hand the
|
|
540
|
+
* work to the cluster. All of them are scoped to `job-list` when given.
|
|
541
|
+
*
|
|
542
|
+
* @param {string} deployList - Comma-separated deploy IDs; in manifest modes its first entry is the manifest owner deploy-id
|
|
543
|
+
* @param {string} jobList - Comma-separated job IDs; in manifest modes it restricts which conf.cron.json jobs are generated
|
|
544
|
+
* @param {Object} options - CLI flags
|
|
545
|
+
* @param {boolean} [options.generateK8sCronjobs] - Generate K8s CronJob YAML manifests
|
|
546
|
+
* @param {boolean} [options.apply] - Apply manifests to the cluster
|
|
547
|
+
* @param {boolean} [options.git] - Pass --git to job execution
|
|
548
|
+
* @param {boolean} [options.dev] - Use local ./ base path instead of global underpost installation
|
|
549
|
+
* @param {string} [options.cmd] - Optional pre-script commands to run before cron execution
|
|
550
|
+
* @param {string} [options.namespace] - Kubernetes namespace
|
|
551
|
+
* @param {string} [options.image] - Custom container image
|
|
552
|
+
* @param {boolean} [options.setupStart] - Update the deploy-id package.json start script and generate+apply its cron jobs
|
|
553
|
+
* @param {boolean} [options.k3s] - Use k3s cluster context (apply directly on host)
|
|
554
|
+
* @param {boolean} [options.kind] - Use kind cluster context (apply via kind-worker container)
|
|
555
|
+
* @param {boolean} [options.kubeadm] - Use kubeadm cluster context (apply directly on host)
|
|
556
|
+
* @param {boolean} [options.dryRun] - Preview cron jobs without executing them
|
|
557
|
+
* @param {boolean} [options.createJobNow] - After applying, immediately create a Job from each CronJob (requires --apply)
|
|
558
|
+
* @param {string} [options.nodeName] - Pin generated CronJob pods to this node (manifest modes only)
|
|
559
|
+
* @memberof UnderpostCron
|
|
560
|
+
*/
|
|
561
|
+
callback: async function (deployList, jobList, options = {}) {
|
|
562
|
+
loadCronDeployEnv();
|
|
563
|
+
|
|
564
|
+
const jobFilter = parseList(jobList);
|
|
565
|
+
|
|
566
|
+
if (options.setupStart) return await Underpost.cron.setupDeployStart(deployList, { ...options, jobFilter });
|
|
567
|
+
|
|
568
|
+
if (options.generateK8sCronjobs || options.apply || options.createJobNow)
|
|
569
|
+
return await Underpost.cron.generateK8sCronJobs({
|
|
570
|
+
...options,
|
|
571
|
+
deployId: deployList,
|
|
572
|
+
jobFilter,
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
if (options.nodeName)
|
|
576
|
+
logger.warn(`--node-name is a manifest placement flag and has no effect on direct execution`, {
|
|
577
|
+
nodeName: options.nodeName,
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
for (const jobId of jobFilter) {
|
|
581
|
+
const resolvedDeployIdList = resolveJobDeployList(jobId, deployList);
|
|
582
|
+
|
|
583
|
+
if (Underpost.cron.JOB[jobId]) {
|
|
584
|
+
if (options.dryRun) {
|
|
585
|
+
logger.info(`[dry-run] Would execute cron job`, { jobId, deployList: resolvedDeployIdList, options });
|
|
586
|
+
} else {
|
|
587
|
+
logger.info(`Executing cron job`, { jobId, deployList: resolvedDeployIdList, options });
|
|
588
|
+
await Underpost.cron.JOB[jobId].callback(resolvedDeployIdList, options);
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
logger.warn(`Unknown cron job: ${jobId}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Update the package.json start script for the given deploy-id and generate+apply its K8s CronJob manifests.
|
|
598
|
+
*
|
|
599
|
+
* @param {string} [deployList] - Comma-separated deploy IDs; its first entry is the deploy-id whose package.json is updated. Falls back to the dd.cron file
|
|
600
|
+
* @param {Object} [options] - Additional options forwarded to generateK8sCronJobs
|
|
601
|
+
* @param {string[]} [options.jobFilter] - Restrict the setup to these job IDs
|
|
602
|
+
* @param {boolean} [options.createJobNow] - After applying, immediately create a Job from each CronJob
|
|
603
|
+
* @param {boolean} [options.dryRun] - Pass --dry-run=client to kubectl commands
|
|
604
|
+
* @param {boolean} [options.apply] - Whether to apply generated manifests to the cluster
|
|
605
|
+
* @param {boolean} [options.git] - Pass --git flag to cron CLI commands
|
|
606
|
+
* @param {boolean} [options.dev] - Use local ./ base path instead of global underpost installation
|
|
607
|
+
* @param {string} [options.cmd] - Optional pre-script commands to run before cron execution
|
|
608
|
+
* @param {string} [options.nodeName] - Pin every generated CronJob's pod to this node
|
|
609
|
+
* @param {string} [options.namespace] - Kubernetes namespace for the CronJobs
|
|
610
|
+
* @param {string} [options.image] - Custom container image override for the CronJobs
|
|
611
|
+
* @param {boolean} [options.k3s] - k3s cluster context (apply directly on host)
|
|
612
|
+
* @param {boolean} [options.kind] - kind cluster context (apply via kind-worker container)
|
|
613
|
+
* @param {boolean} [options.kubeadm] - kubeadm cluster context (apply directly on host)
|
|
614
|
+
* @memberof UnderpostCron
|
|
615
|
+
*/
|
|
616
|
+
setupDeployStart: async function (deployList, options = {}) {
|
|
617
|
+
// Validated up front: an invalid node name must not leave a rewritten package.json behind.
|
|
618
|
+
const nodeName = options.nodeName;
|
|
619
|
+
const requestedDeployId = deployList;
|
|
620
|
+
const deployId = resolveDeployId(requestedDeployId);
|
|
621
|
+
if (!deployId) {
|
|
622
|
+
logger.warn(
|
|
623
|
+
'Could not resolve deploy-id. Provide it as the deploy-list argument or create engine-private/deploy/dd.cron',
|
|
624
|
+
);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (!requestedDeployId) logger.info(`Resolved cron deploy-id from dd.cron`, { deployId });
|
|
628
|
+
const confDir = `./engine-private/conf/${deployId}`;
|
|
629
|
+
const packageJsonPath = `${confDir}/package.json`;
|
|
630
|
+
const confCronPath = `${confDir}/conf.cron.json`;
|
|
631
|
+
|
|
632
|
+
if (!fs.existsSync(confCronPath)) {
|
|
633
|
+
logger.warn(`conf.cron.json not found for deploy-id: ${deployId}`, { path: confCronPath });
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const confCron = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
|
|
638
|
+
|
|
639
|
+
if (!confCron.jobs || Object.keys(confCron.jobs).length === 0) {
|
|
640
|
+
logger.warn(`No cron jobs configured for deploy-id: ${deployId}`);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const jobFilter = parseList(options.jobFilter);
|
|
645
|
+
const enabledJobs = Object.keys(confCron.jobs).filter(
|
|
646
|
+
(job) => confCron.jobs[job].enabled !== false && (jobFilter.length === 0 || jobFilter.includes(job)),
|
|
647
|
+
);
|
|
648
|
+
if (enabledJobs.length === 0) {
|
|
649
|
+
logger.warn(
|
|
650
|
+
`No enabled cron jobs for deploy-id: ${deployId}`,
|
|
651
|
+
jobFilter.length > 0 ? { jobFilter } : undefined,
|
|
652
|
+
);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Start script only references manifests generateK8sCronJobs actually writes
|
|
657
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
658
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
659
|
+
let startCommand = 'echo "Starting cron jobs..."';
|
|
660
|
+
for (const job of enabledJobs)
|
|
661
|
+
startCommand += ` && kubectl apply -f ./manifests/cronjobs/${deployId}/${deployId}-${job}.yaml`;
|
|
662
|
+
if (!packageJson.scripts) packageJson.scripts = {};
|
|
663
|
+
packageJson.scripts.start = startCommand;
|
|
664
|
+
|
|
665
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 4) + '\n', 'utf8');
|
|
666
|
+
logger.info(`Updated package.json start script for ${deployId}`, { path: packageJsonPath });
|
|
667
|
+
} else {
|
|
668
|
+
logger.warn(`package.json not found for deploy-id: ${deployId}`, { path: packageJsonPath });
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
await Underpost.cron.generateK8sCronJobs({
|
|
672
|
+
deployId,
|
|
673
|
+
jobFilter,
|
|
674
|
+
nodeName,
|
|
675
|
+
namespace: options.namespace,
|
|
676
|
+
image: options.image,
|
|
677
|
+
apply: options.apply,
|
|
678
|
+
createJobNow: options.createJobNow,
|
|
679
|
+
git: !!options.git,
|
|
680
|
+
dev: !!options.dev,
|
|
681
|
+
kubeadm: !!options.kubeadm,
|
|
682
|
+
cmd: options.cmd,
|
|
683
|
+
k3s: !!options.k3s,
|
|
684
|
+
kind: !!options.kind,
|
|
685
|
+
dryRun: !!options.dryRun,
|
|
686
|
+
});
|
|
687
|
+
},
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Generate Kubernetes CronJob YAML manifests from conf.cron.json configuration.
|
|
691
|
+
* Each enabled job produces one CronJob YAML file under manifests/cronjobs/<deployId>/.
|
|
692
|
+
* With --apply the manifests are also applied to the cluster via kubectl.
|
|
693
|
+
*
|
|
694
|
+
* @param {Object} options
|
|
695
|
+
* @param {string} [options.deployId] - Explicit deploy-id (overrides dd.cron file lookup)
|
|
696
|
+
* @param {string|string[]} [options.jobFilter] - Restrict generation/apply to these job IDs (empty means all)
|
|
697
|
+
* @param {boolean} [options.git=false] - Pass --git flag to cron CLI commands
|
|
698
|
+
* @param {boolean} [options.dev=false] - Use local ./ base path instead of global underpost
|
|
699
|
+
* @param {string} [options.cmd] - Optional pre-script commands
|
|
700
|
+
* @param {boolean} [options.apply=false] - kubectl apply generated manifests
|
|
701
|
+
* @param {string} [options.namespace='default'] - Target Kubernetes namespace
|
|
702
|
+
* @param {string} [options.image] - Custom container image override
|
|
703
|
+
* @param {boolean} [options.k3s] - k3s cluster context (apply directly on host); read from the cluster when no context is named
|
|
704
|
+
* @param {boolean} [options.kind] - kind cluster context (apply via kind-worker container); read from the cluster when no context is named
|
|
705
|
+
* @param {boolean} [options.kubeadm] - kubeadm cluster context (apply directly on host); read from the cluster when no context is named
|
|
706
|
+
* @param {boolean} [options.createJobNow=false] - After applying, create a Job from each CronJob immediately
|
|
707
|
+
* @param {boolean} [options.dryRun=false] - Pass --dry-run=client to kubectl commands
|
|
708
|
+
* @param {string} [options.nodeName] - Pin every generated CronJob's pod to this node; defaults to this node
|
|
709
|
+
* when the cluster knows it, because the pods mount its checkout
|
|
710
|
+
* @memberof UnderpostCron
|
|
711
|
+
*/
|
|
712
|
+
generateK8sCronJobs: async function (options = {}) {
|
|
713
|
+
const namespace = options.namespace || 'default';
|
|
714
|
+
const jobDeployId = resolveDeployId(options.deployId);
|
|
715
|
+
|
|
716
|
+
if (!jobDeployId) {
|
|
717
|
+
logger.warn(
|
|
718
|
+
'Could not resolve deploy-id. Provide it as the deploy-list argument or create engine-private/deploy/dd.cron',
|
|
719
|
+
);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const confCronPath = `./engine-private/conf/${jobDeployId}/conf.cron.json`;
|
|
724
|
+
|
|
725
|
+
if (!fs.existsSync(confCronPath)) {
|
|
726
|
+
logger.warn(`Cron configuration not found: ${confCronPath}`);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const confCronConfig = JSON.parse(fs.readFileSync(confCronPath, 'utf8'));
|
|
731
|
+
|
|
732
|
+
if (!confCronConfig.jobs || Object.keys(confCronConfig.jobs).length === 0) {
|
|
733
|
+
logger.info('No cron jobs configured');
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const jobFilter = parseList(options.jobFilter);
|
|
738
|
+
const targetJobs = Object.keys(confCronConfig.jobs).filter(
|
|
739
|
+
(job) => jobFilter.length === 0 || jobFilter.includes(job),
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
if (targetJobs.length === 0) {
|
|
743
|
+
logger.warn(`No cron jobs matched the requested job list`, {
|
|
744
|
+
deployId: jobDeployId,
|
|
745
|
+
jobFilter,
|
|
746
|
+
available: Object.keys(confCronConfig.jobs),
|
|
747
|
+
});
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Placement and cluster context are read from the cluster this run publishes to, so a run
|
|
752
|
+
// that only writes manifests stays offline and portable — the same reason `--apply` is what
|
|
753
|
+
// makes a manifest node-specific: it is the run that knows which node's checkout the pods
|
|
754
|
+
// will mount.
|
|
755
|
+
const nodeName = options.apply === true ? cronNodeNameFactory(options.nodeName) : options.nodeName || '';
|
|
756
|
+
const cluster =
|
|
757
|
+
options.apply === true
|
|
758
|
+
? cronClusterContextFactory(options)
|
|
759
|
+
: { k3s: !!options.k3s, kind: !!options.kind, kubeadm: !!options.kubeadm };
|
|
760
|
+
|
|
761
|
+
const outputDir = `./manifests/cronjobs/${jobDeployId}`;
|
|
762
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
763
|
+
|
|
764
|
+
const generatedFiles = [];
|
|
765
|
+
|
|
766
|
+
for (const job of targetJobs) {
|
|
767
|
+
const jobConfig = confCronConfig.jobs[job];
|
|
768
|
+
|
|
769
|
+
if (jobConfig.enabled === false) {
|
|
770
|
+
// The manifest is an output, not a source: leaving the previous one on disk keeps a
|
|
771
|
+
// stale spec around that a later `kubectl apply -f` would happily deploy — which is how
|
|
772
|
+
// a mount or image this generator has since dropped comes back.
|
|
773
|
+
const disabledPath = `${outputDir}/${jobDeployId}-${job}.yaml`;
|
|
774
|
+
if (fs.existsSync(disabledPath)) {
|
|
775
|
+
fs.removeSync(disabledPath);
|
|
776
|
+
logger.info(`Removed manifest for disabled job: ${disabledPath}`);
|
|
777
|
+
}
|
|
778
|
+
logger.info(`Skipping disabled job: ${job}`);
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const deployIdList = Underpost.cron.getRelatedDeployIdList(job);
|
|
783
|
+
const expression = jobConfig.expression || '0 0 * * *';
|
|
784
|
+
const cronJobName = `${jobDeployId}-${job}`;
|
|
785
|
+
|
|
786
|
+
const yamlContent = cronJobYamlFactory({
|
|
787
|
+
name: cronJobName,
|
|
788
|
+
expression,
|
|
789
|
+
deployList: deployIdList,
|
|
790
|
+
jobList: job,
|
|
791
|
+
image: options.image,
|
|
792
|
+
namespace,
|
|
793
|
+
git: !!options.git,
|
|
794
|
+
dev: !!options.dev,
|
|
795
|
+
cmd: options.cmd,
|
|
796
|
+
suspend: false,
|
|
797
|
+
dryRun: !!options.dryRun,
|
|
798
|
+
...cluster,
|
|
799
|
+
nodeName,
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
const yamlFilePath = `${outputDir}/${cronJobName}.yaml`;
|
|
803
|
+
fs.writeFileSync(yamlFilePath, yamlContent, 'utf8');
|
|
804
|
+
generatedFiles.push(yamlFilePath);
|
|
805
|
+
|
|
806
|
+
logger.info(`Generated CronJob manifest: ${yamlFilePath}`, {
|
|
807
|
+
job,
|
|
808
|
+
expression,
|
|
809
|
+
namespace,
|
|
810
|
+
...(nodeName ? { nodeName } : {}),
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (options.apply) {
|
|
815
|
+
// Publishing is the privileged half: it stages credentials on the node and writes cluster
|
|
816
|
+
// resources. Generating manifests is neither, which is why the gate is here and not around
|
|
817
|
+
// the whole command — a workstation or CI runner has no node role and keeps working.
|
|
818
|
+
const role = Underpost.wireguard.localRole();
|
|
819
|
+
if (role) assertRoleCapability({ role, capability: 'cron-publication', operation: 'cron --apply' });
|
|
820
|
+
|
|
821
|
+
// A nodeSelector naming a node that is not registered leaves every Job Pending at its
|
|
822
|
+
// next fire, silently. Warn rather than throw: the node may join before the schedule.
|
|
823
|
+
if (nodeName && !nodeExists(nodeName))
|
|
824
|
+
logger.warn(`Target node not found on the cluster; pods will stay Pending until it joins`, { nodeName });
|
|
825
|
+
|
|
826
|
+
// The node directory the pods write metrics into, labeled for container access.
|
|
827
|
+
prepareCronHostStorage();
|
|
828
|
+
|
|
829
|
+
// The credentials the job bodies read from the environment. Optional by design: the
|
|
830
|
+
// `envFrom` reference is `optional: true`, so a cluster with neither seed files nor the
|
|
831
|
+
// values in its environment still schedules — the jobs that need a GitHub token simply
|
|
832
|
+
// report their own failure instead of the pod refusing to start.
|
|
833
|
+
for (const name of [cronEnvSecretName, cronSshSecretName])
|
|
834
|
+
if (!Underpost.secret.applyIfPresent(name, namespace)) Underpost.secret.applyFromOriginSeed(name, namespace);
|
|
835
|
+
|
|
836
|
+
// Delete existing CronJobs before applying new ones
|
|
837
|
+
for (const job of targetJobs) {
|
|
838
|
+
const cronJobName = `${jobDeployId}-${job}`;
|
|
839
|
+
shellExec(`kubectl delete cronjob ${cronJobName} --namespace=${namespace} --ignore-not-found`);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Ensure default dockerhub image is loaded on the cluster when no custom image is provided
|
|
843
|
+
if (!options.image) {
|
|
844
|
+
logger.info('Ensuring default image is loaded on cluster');
|
|
845
|
+
Underpost.image.pullDockerHubImage({
|
|
846
|
+
dockerhubImage: 'underpost',
|
|
847
|
+
...cluster,
|
|
848
|
+
dev: !!options.dev,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Sync engine volume to kind-worker node if using kind cluster
|
|
853
|
+
if (cluster.kind) {
|
|
854
|
+
syncEngineToKindWorker();
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
for (const yamlFile of generatedFiles) {
|
|
858
|
+
logger.info(`Applying: ${yamlFile}`);
|
|
859
|
+
shellExec(`kubectl apply -f ${yamlFile}`);
|
|
860
|
+
}
|
|
861
|
+
logger.info('All CronJob manifests applied');
|
|
862
|
+
} else {
|
|
863
|
+
logger.info(`Manifests generated in ${outputDir}. Use --apply to deploy to the cluster.`);
|
|
864
|
+
}
|
|
865
|
+
// Create an immediate Job from each CronJob if requested. Runs after --apply so the
|
|
866
|
+
// Job is always cloned from the manifest this invocation just published.
|
|
867
|
+
if (options.createJobNow) {
|
|
868
|
+
for (const job of targetJobs) {
|
|
869
|
+
const jobConfig = confCronConfig.jobs[job];
|
|
870
|
+
if (jobConfig.enabled === false) continue;
|
|
871
|
+
|
|
872
|
+
const cronJobName = `${jobDeployId}-${job}`
|
|
873
|
+
.toLowerCase()
|
|
874
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
875
|
+
.replace(/--+/g, '-')
|
|
876
|
+
.replace(/^-|-$/g, '')
|
|
877
|
+
.substring(0, 52);
|
|
878
|
+
|
|
879
|
+
if (!cronJobExists(cronJobName, namespace)) {
|
|
880
|
+
logger.warn(`CronJob not found on the cluster, skipping immediate Job`, {
|
|
881
|
+
cronJobName,
|
|
882
|
+
namespace,
|
|
883
|
+
hint: 'add --apply to publish the manifest first',
|
|
884
|
+
});
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const immediateJobName = `${cronJobName}-now-${Date.now()}`.substring(0, 63);
|
|
889
|
+
logger.info(`Creating immediate Job from CronJob: ${cronJobName}`, { jobName: immediateJobName });
|
|
890
|
+
shellExec(`kubectl create job ${immediateJobName} --from=cronjob/${cronJobName} -n ${namespace}`);
|
|
891
|
+
}
|
|
892
|
+
logger.info('All immediate Jobs created');
|
|
893
|
+
}
|
|
894
|
+
},
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Resolve the deploy-id list associated with a given job.
|
|
898
|
+
* Backup jobs read from dd.routes (multiple deploy-ids); others from dd.cron.
|
|
899
|
+
*
|
|
900
|
+
* @param {string} jobId - Job identifier (e.g., 'dns', 'backup')
|
|
901
|
+
* @returns {string} Comma-separated deploy IDs
|
|
902
|
+
* @memberof UnderpostCron
|
|
903
|
+
*/
|
|
904
|
+
getRelatedDeployIdList(jobId) {
|
|
905
|
+
if (jobId === 'backup') {
|
|
906
|
+
const routes = readDeployRoutes();
|
|
907
|
+
if (routes.length > 0) return routes.join(',');
|
|
908
|
+
logger.warn(`Deploy route table not found: ${DEPLOY_ROUTES_PATH}, falling back to the cron deploy-id`);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const cronDeployId = cronDeployIdResolve();
|
|
912
|
+
if (!cronDeployId) logger.warn(`Cron deploy-id not resolved, using default`, { jobId, default: DEFAULT_CRON_ID });
|
|
913
|
+
return cronDeployId || DEFAULT_CRON_ID;
|
|
914
|
+
},
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Get the available cron job handlers.
|
|
918
|
+
* Each handler should have a callback function that executes the job logic.
|
|
919
|
+
* @memberof UnderpostCron
|
|
920
|
+
* @returns {Object} Available cron job handlers
|
|
921
|
+
*/
|
|
922
|
+
get JOB() {
|
|
923
|
+
return UnderpostCron.JOB;
|
|
924
|
+
},
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Get the list of available job IDs.
|
|
928
|
+
* This is derived from the keys of the JOB object.
|
|
929
|
+
* @memberof UnderpostCron
|
|
930
|
+
* @returns {string[]} List of available job IDs
|
|
931
|
+
*/
|
|
932
|
+
getJobsIDs() {
|
|
933
|
+
return Object.keys(UnderpostCron.JOB);
|
|
934
|
+
},
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export default UnderpostCron;
|
|
939
|
+
|
|
940
|
+
export {
|
|
941
|
+
cronCheckoutContextFactory,
|
|
942
|
+
cronDeployIdResolve,
|
|
943
|
+
cronJobYamlFactory,
|
|
944
|
+
engineMirrorContentsFactory,
|
|
945
|
+
loadCronDeployEnv,
|
|
946
|
+
parseList,
|
|
947
|
+
resolveDeployId,
|
|
948
|
+
resolveJobDeployList,
|
|
949
|
+
};
|