@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
package/src/cli/run.js
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* @namespace UnderpostRun
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { daemonProcess,
|
|
7
|
+
import { daemonProcess, pbcopy, shellArgumentFactory, shellCd, shellExec } from '../server/runtime/process.js';
|
|
8
|
+
import { cli, withExecutionProfile } from '../server/build/execution.js';
|
|
9
|
+
import { syncDeployPackages } from '../server/build/package.js';
|
|
10
|
+
import { RUNTIME_STATUS } from '../server/runtime/runtime-status.js';
|
|
8
11
|
import {
|
|
9
12
|
awaitDeployMonitor,
|
|
10
|
-
buildKindPorts,
|
|
11
13
|
clusterContextFactory,
|
|
12
14
|
clusterTypeFactory,
|
|
13
|
-
cronDeployIdResolve,
|
|
14
15
|
dispatchBuildInstanceEnv,
|
|
15
16
|
deployHostsFactory,
|
|
16
17
|
etcHostFactory,
|
|
@@ -21,40 +22,43 @@ import {
|
|
|
21
22
|
exposeTcpPortsFactory,
|
|
22
23
|
gatewayApiEnabledFactory,
|
|
23
24
|
generateSecurePassword,
|
|
24
|
-
getNpmRootPath,
|
|
25
25
|
instanceHttpRouteRulesFactory,
|
|
26
26
|
instanceInterceptStatusesFactory,
|
|
27
|
+
instanceEnvFilePath,
|
|
27
28
|
instancePortFactory,
|
|
28
29
|
instanceProxyRoutesFactory,
|
|
29
30
|
instanceStatusPageEntriesFactory,
|
|
30
|
-
isDeployRunnerContext,
|
|
31
31
|
loadConfInstances,
|
|
32
32
|
loadProjectInstanceEnvBuilder,
|
|
33
33
|
loadConfServerJson,
|
|
34
34
|
loadReplicas,
|
|
35
|
-
resolveDeployList,
|
|
36
35
|
resolveEnvScoped,
|
|
37
36
|
selectConfInstances,
|
|
38
37
|
waitForPort,
|
|
39
|
-
writeEnv,
|
|
40
38
|
clusterInstancesFactory,
|
|
41
39
|
deployTrafficEntriesFactory,
|
|
42
|
-
|
|
40
|
+
publicIngressProbeFactory,
|
|
43
41
|
hostIngressFactsFactory,
|
|
44
42
|
hostRenderInstancesFactory,
|
|
45
43
|
instanceTrafficPlanFactory,
|
|
46
44
|
trafficTableRowsFactory,
|
|
47
45
|
isTrafficServingFactory,
|
|
48
46
|
nextTrafficFactory,
|
|
47
|
+
redeployPlanFactory,
|
|
49
48
|
stopPlanFactory,
|
|
50
49
|
trafficFromRoutingInfoFactory,
|
|
51
|
-
} from '../server/conf.js';
|
|
52
|
-
import {
|
|
50
|
+
} from '../server/runtime/conf.js';
|
|
51
|
+
import { buildKindPorts, deployRoutesExists, readDeployRoutes, resolveDeployList } from '../server/network/router.js';
|
|
52
|
+
import { cronDeployIdResolve } from '../server/ops/cron.js';
|
|
53
|
+
import { deployEnvFactory, getNpmRootPath, writeEnv } from '../server/runtime/environment.js';
|
|
54
|
+
import { actionInitLog, loggerFactory } from '../server/ops/logger.js';
|
|
53
55
|
|
|
54
56
|
import fs from 'fs-extra';
|
|
55
57
|
import { range, s4, setPad, timer } from '../client/components/core/CommonJs.js';
|
|
56
58
|
|
|
57
59
|
import os from 'os';
|
|
60
|
+
import nodePath from 'node:path';
|
|
61
|
+
import { domainContextFactory } from './domains.js';
|
|
58
62
|
import Underpost from '../index.js';
|
|
59
63
|
import dotenv from 'dotenv';
|
|
60
64
|
import { MongoBootstrap } from '../db/mongo/MongoBootstrap.js';
|
|
@@ -70,7 +74,7 @@ import {
|
|
|
70
74
|
readHostInstanceRegistry,
|
|
71
75
|
writeHostInstanceRegistry,
|
|
72
76
|
writeHostServerConf,
|
|
73
|
-
} from '../server/underpost-gateway.js';
|
|
77
|
+
} from '../server/network/underpost-gateway.js';
|
|
74
78
|
const logger = loggerFactory(import.meta);
|
|
75
79
|
|
|
76
80
|
/**
|
|
@@ -90,6 +94,9 @@ const logger = loggerFactory(import.meta);
|
|
|
90
94
|
* @property {string} volumeHostPath - The host path for the volume.
|
|
91
95
|
* @property {string} volumeMountPath - The mount path for the volume.
|
|
92
96
|
* @property {string} imageName - The name of the image to run.
|
|
97
|
+
* @property {string} runtimeImage - `src/runtime/<name>` image family the cluster runner brings up (`cluster`).
|
|
98
|
+
* @property {string} image - Container image the deployment pulls and runs (`sync`).
|
|
99
|
+
* @property {string} versions - Comma-separated blue/green deployment versions (`sync`).
|
|
93
100
|
* @property {string} containerName - The name of the container to run.
|
|
94
101
|
* @property {string} namespace - The namespace to run in.
|
|
95
102
|
* @property {string} timeoutResponse - The response timeout duration.
|
|
@@ -127,7 +134,7 @@ const logger = loggerFactory(import.meta);
|
|
|
127
134
|
* @property {string} confServerPath - The configuration server path.
|
|
128
135
|
* @property {string} underpostRoot - The root path of the Underpost installation.
|
|
129
136
|
* @property {string} cmdCronJobs - Pre-script commands to run before cron job execution.
|
|
130
|
-
* @property {string} deployIdCronJobs -
|
|
137
|
+
* @property {string} deployIdCronJobs - Cron deploy-id passed to `cron --setup-start`; unset resolves dd.cron, `none` skips cron setup.
|
|
131
138
|
* @property {string} timezone - The timezone to set.
|
|
132
139
|
* @property {boolean} kubeadm - Whether to run in kubeadm mode.
|
|
133
140
|
* @property {boolean} kind - Whether to run in kind mode.
|
|
@@ -138,6 +145,7 @@ const logger = loggerFactory(import.meta);
|
|
|
138
145
|
* @property {string} user - The user to run as.
|
|
139
146
|
* @property {string} group - The group to use.
|
|
140
147
|
* @property {string} pid - The process ID.
|
|
148
|
+
* @property {string} repoEnginePrivate - Private configuration repository the pull runner checks out, overriding the pairing derived from the engine source.
|
|
141
149
|
* @property {boolean} disablePrivateConfUpdate - Whether to disable private configuration updates.
|
|
142
150
|
* @property {string} monitorStatus - The monitor status option.
|
|
143
151
|
* @property {string} monitorStatusKindType - The monitor status kind type option.
|
|
@@ -184,6 +192,9 @@ const DEFAULT_OPTION = {
|
|
|
184
192
|
volumeHostPath: '',
|
|
185
193
|
volumeMountPath: '',
|
|
186
194
|
imageName: '',
|
|
195
|
+
image: '',
|
|
196
|
+
runtimeImage: '',
|
|
197
|
+
versions: '',
|
|
187
198
|
containerName: '',
|
|
188
199
|
namespace: 'default',
|
|
189
200
|
timeoutResponse: '',
|
|
@@ -232,6 +243,7 @@ const DEFAULT_OPTION = {
|
|
|
232
243
|
user: '',
|
|
233
244
|
group: '',
|
|
234
245
|
pid: '',
|
|
246
|
+
repoEnginePrivate: '',
|
|
235
247
|
disablePrivateConfUpdate: false,
|
|
236
248
|
monitorStatus: '',
|
|
237
249
|
monitorStatusKindType: '',
|
|
@@ -272,33 +284,6 @@ const DEFAULT_OPTION = {
|
|
|
272
284
|
* @memberof UnderpostRun
|
|
273
285
|
*/
|
|
274
286
|
|
|
275
|
-
// Secrets `sops-setup` onboards when no explicit list is passed: the full self-hosted data tier.
|
|
276
|
-
// `mongodb-keyfile` is listed alongside `mongodb-secret` because the MongoDB StatefulSet mounts
|
|
277
|
-
// it as a volume for intra-replica-set auth and will not start without it, so onboarding the
|
|
278
|
-
// credentials alone would leave Mongo broken.
|
|
279
|
-
const SOPS_SETUP_DEFAULT_SECRETS = ['postgres-secret', 'mariadb-secret', 'mongodb-secret', 'mongodb-keyfile'];
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Produces a value for a Secret data key that has no origin seed file and no `--args` override.
|
|
283
|
-
* Key-aware because the data tier does not want one shape of secret: a replica-set keyfile is a
|
|
284
|
-
* long base64 blob, a username is an identifier, and everything else is a password.
|
|
285
|
-
* @param {string} key - Secret data key (e.g. 'password', 'username', 'mongodb-keyfile').
|
|
286
|
-
* @returns {string} Generated value.
|
|
287
|
-
* @memberof UnderpostRun
|
|
288
|
-
*/
|
|
289
|
-
const generateSeedValue = (key) => {
|
|
290
|
-
if (key === 'username') return 'admin';
|
|
291
|
-
// MongoDB keyfile: 6-1024 base64 characters shared by every replica-set member. Newlines are
|
|
292
|
-
// stripped so the value round-trips identically through YAML and through
|
|
293
|
-
// MongoBootstrap.readCredential, which strips them too.
|
|
294
|
-
if (key === 'mongodb-keyfile')
|
|
295
|
-
return shellExec(`openssl rand -base64 756`, { stdout: true, silent: true, disableLog: true }).replace(
|
|
296
|
-
/\r?\n/g,
|
|
297
|
-
'',
|
|
298
|
-
);
|
|
299
|
-
return generateSecurePassword(24);
|
|
300
|
-
};
|
|
301
|
-
|
|
302
287
|
class UnderpostRun {
|
|
303
288
|
/**
|
|
304
289
|
* @static
|
|
@@ -327,13 +312,12 @@ class UnderpostRun {
|
|
|
327
312
|
throw new Error(`Invalid Kubernetes node name: ${options.nodeName}`);
|
|
328
313
|
const env = options.dev ? 'development' : 'production';
|
|
329
314
|
const requestedDeploys = `${path || options.deployId || ''}`.trim();
|
|
330
|
-
const routerPath = './engine-private/deploy/dd.router';
|
|
331
315
|
const confRoot = './engine-private/conf';
|
|
332
316
|
const deployIds = [
|
|
333
317
|
...new Set(
|
|
334
318
|
requestedDeploys
|
|
335
319
|
? resolveDeployList(requestedDeploys)
|
|
336
|
-
:
|
|
320
|
+
: deployRoutesExists()
|
|
337
321
|
? resolveDeployList('dd')
|
|
338
322
|
: fs.existsSync(confRoot)
|
|
339
323
|
? fs
|
|
@@ -489,7 +473,7 @@ class UnderpostRun {
|
|
|
489
473
|
* @memberof UnderpostRun
|
|
490
474
|
*/
|
|
491
475
|
'dev-cluster': (path, options = DEFAULT_OPTION) => {
|
|
492
|
-
const baseCommand =
|
|
476
|
+
const baseCommand = cli('underpost', { local: true });
|
|
493
477
|
const mongoHosts = ['mongodb-0.mongodb-service'];
|
|
494
478
|
let primaryMongoHost = 'mongodb-0.mongodb-service';
|
|
495
479
|
const clusterType = clusterTypeFactory(options);
|
|
@@ -827,52 +811,21 @@ class UnderpostRun {
|
|
|
827
811
|
return results;
|
|
828
812
|
},
|
|
829
813
|
|
|
830
|
-
/**
|
|
831
|
-
* @method dev-hosts-expose
|
|
832
|
-
* @description Deploys a specified service in development mode with `/etc/hosts` modification for local access.
|
|
833
|
-
* @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID to deploy).
|
|
834
|
-
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
835
|
-
* @memberof UnderpostRun
|
|
836
|
-
*/
|
|
837
|
-
'dev-hosts-expose': async (path, options = DEFAULT_OPTION) => {
|
|
838
|
-
shellExec(`node bin deploy ${path} development --disable-update-deployment --disable-update-proxy --kubeadm`);
|
|
839
|
-
// /etc/hosts is written here, not by `deploy`: that command has no
|
|
840
|
-
// --etc-hosts option, and the `etc-hosts` runner already resolves a
|
|
841
|
-
// deploy's hosts from its conf.server.json.
|
|
842
|
-
await UnderpostRun.RUNNERS['etc-hosts']('', { ...options, deployId: path });
|
|
843
|
-
},
|
|
844
|
-
|
|
845
|
-
/**
|
|
846
|
-
* @method dev-hosts-restore
|
|
847
|
-
* @description Restores the `/etc/hosts` file to its original state after modifications made during development deployments.
|
|
848
|
-
* @param {string} path - The input value, identifier, or path for the operation.
|
|
849
|
-
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
850
|
-
* @memberof UnderpostRun
|
|
851
|
-
*/
|
|
852
|
-
'dev-hosts-restore': (path, options = DEFAULT_OPTION) => {
|
|
853
|
-
// Rewrite /etc/hosts with the loopback block alone, dropping the deploy
|
|
854
|
-
// host entries `dev-hosts-expose` (and the `cluster` runner) added.
|
|
855
|
-
const hostListenResult = etcHostFactory([]);
|
|
856
|
-
logger.info(hostListenResult.renderHosts);
|
|
857
|
-
},
|
|
858
|
-
|
|
859
814
|
/**
|
|
860
815
|
* @method cluster-build
|
|
861
|
-
* @description
|
|
862
|
-
*
|
|
816
|
+
* @description Resets the workspace and regenerates every routed deploy's default
|
|
817
|
+
* configuration, optionally committing the result to both repositories.
|
|
818
|
+
*
|
|
819
|
+
* Single-replica materialization is not done here: `deploy --sync` builds those folders
|
|
820
|
+
* as part of the deploy itself (`getDataDeploy({ buildSingleReplica: true })`).
|
|
821
|
+
* @param {string} path - `cmt` to commit the regenerated configuration; otherwise unused.
|
|
863
822
|
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
864
823
|
* @memberof UnderpostRun
|
|
865
824
|
*/
|
|
866
825
|
'cluster-build': (path, options = DEFAULT_OPTION) => {
|
|
867
|
-
const nodeOptions =
|
|
868
|
-
(options.nodeName ? ` --node-name ${options.nodeName}` : '') +
|
|
869
|
-
(options.sshKeyPath ? ` --ssh-key-path ${options.sshKeyPath}` : '');
|
|
870
826
|
shellExec(`node bin run clean`);
|
|
871
|
-
shellExec(`node bin
|
|
872
|
-
shellExec(`node bin
|
|
873
|
-
shellExec(`node bin env clean`);
|
|
874
|
-
for (const deployId of fs.readFileSync('./engine-private/deploy/dd.router', 'utf8').split(','))
|
|
875
|
-
shellExec(`node bin new --default-conf --deploy-id ${deployId.trim()}`);
|
|
827
|
+
shellExec(`node bin app clean`);
|
|
828
|
+
for (const deployId of readDeployRoutes()) shellExec(`node bin new --default-conf --deploy-id ${deployId}`);
|
|
876
829
|
if (path === 'cmt') {
|
|
877
830
|
shellExec(`git add . && underpost cmt . build cluster-build`);
|
|
878
831
|
shellExec(`cd engine-private && git add . && underpost cmt . build cluster-build`);
|
|
@@ -888,14 +841,14 @@ class UnderpostRun {
|
|
|
888
841
|
* @memberof UnderpostRun
|
|
889
842
|
*/
|
|
890
843
|
'template-deploy': (path = '', options = DEFAULT_OPTION) => {
|
|
891
|
-
const baseCommand =
|
|
844
|
+
const baseCommand = cli('underpost', { local: true });
|
|
892
845
|
shellExec(`npm run security:secrets`);
|
|
893
846
|
const reportPath = './gitleaks-report.json';
|
|
894
847
|
if (fs.existsSync(reportPath) && JSON.parse(fs.readFileSync(reportPath, 'utf8')).length > 0) {
|
|
895
848
|
logger.error('Secrets detected in gitleaks-report.json, aborting template-deploy');
|
|
896
849
|
return;
|
|
897
850
|
}
|
|
898
|
-
|
|
851
|
+
Underpost.repo.fastForwardEnginePair(baseCommand);
|
|
899
852
|
shellExec(`${baseCommand} run shared-dir`);
|
|
900
853
|
|
|
901
854
|
// Capture the sanitized message from the last N commits (--from-n-commit, default 1) for
|
|
@@ -965,14 +918,14 @@ class UnderpostRun {
|
|
|
965
918
|
* @memberof UnderpostRun
|
|
966
919
|
*/
|
|
967
920
|
'template-deploy-local': async (path, options = DEFAULT_OPTION) => {
|
|
968
|
-
const baseCommand =
|
|
921
|
+
const baseCommand = cli('underpost', { local: true });
|
|
969
922
|
shellExec(`npm run security:secrets`);
|
|
970
923
|
const reportPath = './gitleaks-report.json';
|
|
971
924
|
if (fs.existsSync(reportPath) && JSON.parse(fs.readFileSync(reportPath, 'utf8')).length > 0) {
|
|
972
925
|
logger.error('Secrets detected in gitleaks-report.json, aborting template-deploy');
|
|
973
926
|
return;
|
|
974
927
|
}
|
|
975
|
-
|
|
928
|
+
Underpost.repo.fastForwardEnginePair(baseCommand);
|
|
976
929
|
shellExec(`${baseCommand} run shared-dir`);
|
|
977
930
|
|
|
978
931
|
// Capture the sanitized message from the last N commits (--from-n-commit, default 1).
|
|
@@ -1016,47 +969,51 @@ class UnderpostRun {
|
|
|
1016
969
|
},
|
|
1017
970
|
/**
|
|
1018
971
|
* @method pull
|
|
1019
|
-
* @description
|
|
1020
|
-
*
|
|
972
|
+
* @description Brings the node's engine checkout and its private configuration to the tip of
|
|
973
|
+
* their repositories, at `/home/dd/engine` and `/home/dd/engine/engine-private`.
|
|
974
|
+
*
|
|
975
|
+
* With no argument this is the monorepo pair, `engine` and `engine-private`. Naming a source
|
|
976
|
+
* repository deploys that source instead, and its private conf repository is derived from the
|
|
977
|
+
* conf id they share rather than passed separately — `underpostnet/engine-test-lampp` pairs
|
|
978
|
+
* with `underpostnet/engine-lampp-private`, as does `underpostnet/engine-lampp`.
|
|
979
|
+
* `--repo-engine-private` overrides that derivation for a conf repository the pairing cannot
|
|
980
|
+
* name, and is the flag `deploy/lib/host.sh` forwards `ENGINE_SRC_PRIVATE_REPO` through.
|
|
981
|
+
*
|
|
982
|
+
* Both checkouts are replaced rather than merged, by the same `--switch-repo` route the fleet
|
|
983
|
+
* sync takes — so a node moves onto a test source repo and back without being reprovisioned,
|
|
984
|
+
* and one that has drifted onto commits of its own is brought back instead of refused.
|
|
985
|
+
*
|
|
986
|
+
* The replacement checkout is installed before this returns: it is a different commit than
|
|
987
|
+
* the one the caller started from, and nothing that follows can run against uninstalled
|
|
988
|
+
* dependencies.
|
|
989
|
+
* @param {string} path - Engine source repository, as `owner/repo` or a clone URL. Defaults to
|
|
990
|
+
* the `GITHUB_USERNAME` monorepo.
|
|
1021
991
|
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
1022
992
|
* @memberof UnderpostRun
|
|
1023
993
|
*/
|
|
1024
|
-
pull: (path, options = DEFAULT_OPTION) => {
|
|
994
|
+
pull: (path = '', options = DEFAULT_OPTION) => {
|
|
1025
995
|
// shellExec is fail-fast by default — any non-zero exit throws and
|
|
1026
996
|
// propagates up to the workflow step. No per-call flag required.
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
* @method release-deploy
|
|
1046
|
-
* @description Executes deployment (`underpost run deploy`) for all deployment IDs listed in `./engine-private/deploy/dd.router`.
|
|
1047
|
-
* @param {string} path - The input value, identifier, or path for the operation.
|
|
1048
|
-
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
1049
|
-
* @memberof UnderpostRun
|
|
1050
|
-
*/
|
|
1051
|
-
'release-deploy': (path, options = DEFAULT_OPTION) => {
|
|
1052
|
-
actionInitLog();
|
|
1053
|
-
shellExec(`underpost --version`);
|
|
1054
|
-
shellCd(`/home/dd/engine`);
|
|
1055
|
-
for (const _deployId of fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',')) {
|
|
1056
|
-
const deployId = _deployId.trim();
|
|
1057
|
-
shellExec(`underpost run deploy ${deployId}`, { async: true });
|
|
1058
|
-
}
|
|
997
|
+
const { engine: source, enginePrivate: privateRepo } = Underpost.repo.enginePairFactory({
|
|
998
|
+
engine: path,
|
|
999
|
+
enginePrivate: options.repoEnginePrivate,
|
|
1000
|
+
});
|
|
1001
|
+
logger.info('Pulling engine source', { source, privateRepo });
|
|
1002
|
+
|
|
1003
|
+
const engineRoot = `/home/dd/engine`;
|
|
1004
|
+
// Cleaning is the in-process API rather than a shell round trip through the
|
|
1005
|
+
// global `underpost`: that bin is linked at the very checkout being replaced,
|
|
1006
|
+
// so a tree left without its dependencies made the pre-clean — and with it the
|
|
1007
|
+
// whole pull — exit non-zero on exactly the node that needed replacing.
|
|
1008
|
+
if (fs.existsSync(engineRoot)) Underpost.repo.clean({ paths: [engineRoot, `${engineRoot}/engine-private`] });
|
|
1009
|
+
Underpost.repo.syncCheckout({ path: engineRoot, repo: source });
|
|
1010
|
+
Underpost.repo.syncCheckout({ path: `${engineRoot}/engine-private`, repo: privateRepo });
|
|
1011
|
+
// The checkout that lands here is a different commit than the one this process
|
|
1012
|
+
// started from, and its package.json is only a description until it is installed.
|
|
1013
|
+
// Everything after this — the linked global bin included — runs from this tree.
|
|
1014
|
+
shellExec(`cd ${engineRoot} && npm install`);
|
|
1059
1015
|
},
|
|
1016
|
+
|
|
1060
1017
|
/**
|
|
1061
1018
|
* @method ssh-deploy
|
|
1062
1019
|
* @description Dispatches the corresponding CD workflow for SSH-based deployment, replacing empty commits with workflow_dispatch.
|
|
@@ -1126,36 +1083,35 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
1126
1083
|
},
|
|
1127
1084
|
/**
|
|
1128
1085
|
* @method sync
|
|
1129
|
-
* @description Cleans up, and then runs a deployment synchronization command (`underpost deploy --kubeadm --build-manifest --sync...`)
|
|
1086
|
+
* @description Cleans up, and then runs a deployment synchronization command (`underpost deploy --kubeadm --build-manifest --sync...`).
|
|
1087
|
+
*
|
|
1088
|
+
* Every input is a flag: `--deploy-id`, `--replicas`, `--versions`, `--image`, `--node-name`.
|
|
1130
1089
|
*
|
|
1131
1090
|
* Forwards `--image-pull-policy <policy>` to the underlying `deploy --build-manifest` invocation when `options.imagePullPolicy` is set,
|
|
1132
1091
|
* which then plumbs through `buildManifest` and `deploymentYamlPartsFactory` to override the container `imagePullPolicy` in the generated
|
|
1133
1092
|
* `deployment.yaml`. Useful when you want to force `Always` so the kubelet re-pulls a mutable tag on every rollout. Example:
|
|
1134
|
-
* `node bin run sync dd-core --kubeadm --image-pull-policy Always`
|
|
1135
|
-
*
|
|
1093
|
+
* `node bin run sync --deploy-id dd-core --kubeadm --image-pull-policy Always`
|
|
1094
|
+
*
|
|
1095
|
+
* Runs in one of two modes. Given `--deploy-id`, it deploys. Passed `--build`, or no
|
|
1096
|
+
* `--deploy-id` at all, it renders the manifests and the router table and stops before
|
|
1097
|
+
* touching the cluster:
|
|
1098
|
+
* `node bin run --dev --build sync --deploy-id dd-default`
|
|
1099
|
+
* @param {string} [path] - Unused; every input is a flag.
|
|
1136
1100
|
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
1137
1101
|
* @memberof UnderpostRun
|
|
1138
1102
|
*/
|
|
1139
1103
|
sync: async (path, options = DEFAULT_OPTION) => {
|
|
1140
|
-
|
|
1141
|
-
const env = options.dev ? 'development' : 'production';
|
|
1104
|
+
const env = deployEnvFactory(options);
|
|
1142
1105
|
options = { ...options, gatewayApi: gatewayApiEnabledFactory(options) };
|
|
1143
|
-
const baseCommand = '
|
|
1106
|
+
const baseCommand = cli('underpost', { local: true });
|
|
1144
1107
|
const baseClusterCommand = options.dev ? ' --dev' : '';
|
|
1145
1108
|
const clusterFlag = options.k3s ? ' --k3s' : options.kind ? ' --kind' : ' --kubeadm';
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
];
|
|
1153
|
-
let [deployId, replicas, versions, image, node] = path ? path.split(',') : defaultPath;
|
|
1154
|
-
deployId = deployId ? deployId : defaultPath[0];
|
|
1155
|
-
replicas = replicas ? replicas : defaultPath[1];
|
|
1156
|
-
versions = versions ? versions.replaceAll('+', ',') : defaultPath[2];
|
|
1157
|
-
image = image ? image : defaultPath[3];
|
|
1158
|
-
node = node ? node : options.nodeName ? options.nodeName : defaultPath[4];
|
|
1109
|
+
const deploying = !options.build && !!options.deployId;
|
|
1110
|
+
const deployId = options.deployId || 'dd-default';
|
|
1111
|
+
const replicas = options.replicas || 1;
|
|
1112
|
+
const image = options.image || '';
|
|
1113
|
+
const node = options.nodeName || (!options.kubeadm && !options.k3s ? 'kind-control-plane' : os.hostname());
|
|
1114
|
+
let versions = options.versions || '';
|
|
1159
1115
|
shellExec(`${baseCommand} cluster --ns-use ${options.namespace}`);
|
|
1160
1116
|
|
|
1161
1117
|
if (image && !image.startsWith('localhost'))
|
|
@@ -1166,17 +1122,25 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
1166
1122
|
k3s: options.k3s,
|
|
1167
1123
|
});
|
|
1168
1124
|
|
|
1169
|
-
if (
|
|
1125
|
+
if (deploying) {
|
|
1170
1126
|
if (!options.disablePrivateConfUpdate) {
|
|
1171
1127
|
const { validVersion } = Underpost.repo.privateConfUpdate(deployId);
|
|
1172
1128
|
if (!validVersion) throw new Error('Version mismatch');
|
|
1173
1129
|
}
|
|
1174
1130
|
if (options.timezone !== 'none') shellExec(`${baseCommand} run${baseClusterCommand} tz`);
|
|
1175
|
-
if (options.deployIdCronJobs !== 'none')
|
|
1176
|
-
|
|
1131
|
+
if (options.deployIdCronJobs !== 'none') {
|
|
1132
|
+
// --deploy-id-cron-jobs overrides the cron deploy-id; unset falls back to
|
|
1133
|
+
// dd.cron, the same source `cron --setup-start` resolves against on its own.
|
|
1134
|
+
const cronDeployId = options.deployIdCronJobs || cronDeployIdResolve() || '';
|
|
1135
|
+
if (cronDeployId && !/^[a-zA-Z0-9._,-]+$/.test(cronDeployId))
|
|
1136
|
+
throw new Error(`Invalid cron deploy-id: ${cronDeployId}`);
|
|
1137
|
+
shellExec(
|
|
1138
|
+
`node bin cron${cronDeployId ? ` ${cronDeployId}` : ''}${baseClusterCommand}${clusterFlag} --setup-start --git --apply`,
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1177
1141
|
}
|
|
1178
1142
|
|
|
1179
|
-
const currentTraffic =
|
|
1143
|
+
const currentTraffic = deploying
|
|
1180
1144
|
? Underpost.deploy.getCurrentTraffic(deployId, {
|
|
1181
1145
|
namespace: options.namespace,
|
|
1182
1146
|
env,
|
|
@@ -1200,14 +1164,12 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
1200
1164
|
});
|
|
1201
1165
|
|
|
1202
1166
|
const ignorePods =
|
|
1203
|
-
|
|
1167
|
+
deploying && targetTraffic
|
|
1204
1168
|
? Underpost.kubectl.get(`${deployId}-${env}-${targetTraffic}`, 'pods', options.namespace).map((p) => p.NAME)
|
|
1205
1169
|
: [];
|
|
1206
1170
|
|
|
1207
1171
|
const timeoutFlags = Underpost.deploy.timeoutFlagsFactory(options);
|
|
1208
|
-
const cmdString = options.cmd
|
|
1209
|
-
? ' --cmd ' + (options.cmd.find((c) => c.match('"')) ? '"' + options.cmd + '"' : "'" + options.cmd + "'")
|
|
1210
|
-
: '';
|
|
1172
|
+
const cmdString = options.cmd ? ` --cmd ${shellArgumentFactory(options.cmd)}` : '';
|
|
1211
1173
|
const gitCleanFlag = options.gitClean ? ' --git-clean' : '';
|
|
1212
1174
|
|
|
1213
1175
|
const skipFullBuildFlag = options.skipFullBuild ? ' --skip-full-build' : '';
|
|
@@ -1216,11 +1178,22 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
1216
1178
|
const sshKeyPathFlag = options.sshKeyPath ? ` --ssh-key-path ${options.sshKeyPath}` : '';
|
|
1217
1179
|
const gatewayApiFlags = Underpost.deploy.gatewayApiFlagsFactory(options);
|
|
1218
1180
|
|
|
1181
|
+
// Retarget `underpost-config` at this sync's environment before anything is
|
|
1182
|
+
// deployed. `underpost host load` reads it back inside the
|
|
1183
|
+
// pod, so a stale Secret makes the container resolve the wrong NODE_ENV.
|
|
1184
|
+
if (deploying) {
|
|
1185
|
+
Underpost.host.apply(domainContextFactory({ env, namespace: options.namespace }));
|
|
1186
|
+
Underpost.app.apply(
|
|
1187
|
+
domainContextFactory({ env, namespace: options.namespace, args: { 'deploy-id': deployId } }),
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1219
1191
|
// A direct sync owns the same gateway-first contract as the full cluster
|
|
1220
1192
|
// runner. Build the host-side SSR documents before generating routes unless
|
|
1221
|
-
// the caller explicitly selected a pre-built bundle workflow.
|
|
1222
|
-
|
|
1223
|
-
|
|
1193
|
+
// the caller explicitly selected a pre-built bundle workflow. `--env` is
|
|
1194
|
+
// explicit: the SSR pass must render for the target environment, never for
|
|
1195
|
+
// whatever NODE_ENV the calling shell happens to carry.
|
|
1196
|
+
if (deploying && !options.skipFullBuild) shellExec(`${baseCommand} client ${deployId} --env ${env} --ssr`);
|
|
1224
1197
|
|
|
1225
1198
|
shellExec(
|
|
1226
1199
|
`${baseCommand} deploy${clusterFlag} --build-manifest --sync --info-router --replicas ${replicas} --node ${node}${
|
|
@@ -1230,7 +1203,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
1230
1203
|
}${timeoutFlags}${cmdString}${gitCleanFlag}${skipFullBuildFlag}${pullBundleFlag}${imagePullPolicyFlag}${sshKeyPathFlag}${gatewayApiFlags} ${deployId} ${env}`,
|
|
1231
1204
|
);
|
|
1232
1205
|
|
|
1233
|
-
if (
|
|
1206
|
+
if (deploying) {
|
|
1234
1207
|
if (options.gatewayApi) {
|
|
1235
1208
|
const namespace = options.namespace || 'default';
|
|
1236
1209
|
const gatewayRoot = Underpost.deploy.underpostGatewayRootFactory(options);
|
|
@@ -1301,6 +1274,48 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
1301
1274
|
);
|
|
1302
1275
|
},
|
|
1303
1276
|
|
|
1277
|
+
/**
|
|
1278
|
+
* @method net-tables
|
|
1279
|
+
* @description Prints the host's network tables: interfaces and links (`ip -br link show`) with state, MAC address, and flags, followed by IP addresses (`ip -br addr show`) with IPv4/IPv6, each aligned via `column -t`.
|
|
1280
|
+
* @param {string} path - The input value, identifier, or path for the operation (unused).
|
|
1281
|
+
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow (unused).
|
|
1282
|
+
* @memberof UnderpostRun
|
|
1283
|
+
*/
|
|
1284
|
+
'net-tables': (path, options = DEFAULT_OPTION) => {
|
|
1285
|
+
logger.info(`INTERFACES & LINKS`);
|
|
1286
|
+
shellExec(`(echo "INTERFACE STATE MAC FLAGS"; ip -br link show) | column -t`);
|
|
1287
|
+
logger.info(`IP ADDRESSES`);
|
|
1288
|
+
shellExec(`(echo "INTERFACE STATE IPV4 IPV6"; ip -br addr show) | column -t`);
|
|
1289
|
+
logger.info('KERNEL ROUTING TABLE');
|
|
1290
|
+
shellExec(`(echo "DESTINATION GATEWAY INTERFACE PROTO SCOPE METRIC"; \
|
|
1291
|
+
ip route show | awk '{
|
|
1292
|
+
dst = $1; gw = "on-link"; dev = "-"; proto = "-"; scope = "-"; metric = "-";
|
|
1293
|
+
for (i=1; i<=NF; i++) {
|
|
1294
|
+
if ($i == "via") gw = $(i+1);
|
|
1295
|
+
if ($i == "dev") dev = $(i+1);
|
|
1296
|
+
if ($i == "proto") proto = $(i+1);
|
|
1297
|
+
if ($i == "scope") scope = $(i+1);
|
|
1298
|
+
if ($i == "metric") metric = $(i+1);
|
|
1299
|
+
}
|
|
1300
|
+
print dst, gw, dev, proto, scope, metric;
|
|
1301
|
+
}') | column -t`);
|
|
1302
|
+
logger.info(`ACTIVE NAT FLOWS`);
|
|
1303
|
+
shellExec(`(echo "CHAIN TYPE TARGET_IP_PORT PACKETS BYTES"; \
|
|
1304
|
+
sudo nft list table ip nat | awk '
|
|
1305
|
+
/chain (PREROUTING|POSTROUTING)/ {chain=$2}
|
|
1306
|
+
/dnat to|masquerade|snat to/ && !/packets 0/ {
|
|
1307
|
+
type = ($0 ~ "dnat") ? "DNAT" : (($0 ~ "masquerade") ? "MASQUERADE" : "SNAT");
|
|
1308
|
+
target = "N/A";
|
|
1309
|
+
pkts = "0"; bytes = "0";
|
|
1310
|
+
for(i=1; i<=NF; i++) {
|
|
1311
|
+
if ($i == "packets") pkts = $(i+1);
|
|
1312
|
+
if ($i == "bytes") bytes = $(i+1);
|
|
1313
|
+
if ($i == "to") target = $(i+1);
|
|
1314
|
+
}
|
|
1315
|
+
print chain, type, target, pkts, bytes;
|
|
1316
|
+
}') | column -t`);
|
|
1317
|
+
},
|
|
1318
|
+
|
|
1304
1319
|
/**
|
|
1305
1320
|
* @method stop
|
|
1306
1321
|
* @description Deletes colour-suffixed Deployments and their Services, leaving routing untouched.
|
|
@@ -1412,8 +1427,8 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
1412
1427
|
? options.timezone
|
|
1413
1428
|
: path
|
|
1414
1429
|
? path
|
|
1415
|
-
: Underpost.
|
|
1416
|
-
? Underpost.
|
|
1430
|
+
: Underpost.host.store.get('TIME_ZONE', undefined, { disableLog: true })
|
|
1431
|
+
? Underpost.host.store.get('TIME_ZONE')
|
|
1417
1432
|
: process.env.TIME_ZONE
|
|
1418
1433
|
? process.env.TIME_ZONE
|
|
1419
1434
|
: 'America/New_York';
|
|
@@ -1556,7 +1571,6 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
1556
1571
|
// Probe the exact public URL represented by each host/path row. PWA rows
|
|
1557
1572
|
// can carry several configured paths in one cell, while instance rows
|
|
1558
1573
|
// carry one; cache by URL so shared rows never repeat network work.
|
|
1559
|
-
const shellArg = (value) => `'${`${value}`.replaceAll("'", "'\\''")}'`;
|
|
1560
1574
|
const probeCache = new Map();
|
|
1561
1575
|
const probePath = (host, routePath, tls) => {
|
|
1562
1576
|
const normalizedPath = `${routePath || '/'}`.startsWith('/') ? `${routePath || '/'}` : `/${routePath}`;
|
|
@@ -1566,18 +1580,8 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
1566
1580
|
} catch {
|
|
1567
1581
|
return { path: normalizedPath, url: '', statuses: ['000'] };
|
|
1568
1582
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
// per hop, -i keeps full response headers available, and -s removes
|
|
1572
|
-
// only the progress meter. The body is discarded to keep a status
|
|
1573
|
-
// report bounded even when a host returns a large application page.
|
|
1574
|
-
const raw = shellExec(
|
|
1575
|
-
`curl -L -v -i -s --connect-timeout 3 --max-time 12 --max-redirs 10 ` +
|
|
1576
|
-
`-o /dev/null -w '\nUNDERPOST_CURL_FINAL=%{http_code}\n' ${shellArg(url)} 2>&1 || true`,
|
|
1577
|
-
{ stdout: true, silent: true, silentOnError: true, disableLog: true },
|
|
1578
|
-
);
|
|
1579
|
-
probeCache.set(url, curlStatusChainFactory(raw));
|
|
1580
|
-
}
|
|
1583
|
+
// Cache by URL so rows that share one never repeat network work.
|
|
1584
|
+
if (!probeCache.has(url)) probeCache.set(url, publicIngressProbeFactory(url));
|
|
1581
1585
|
return { path: normalizedPath, url, statuses: probeCache.get(url) };
|
|
1582
1586
|
};
|
|
1583
1587
|
|
|
@@ -1618,7 +1622,7 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
1618
1622
|
|
|
1619
1623
|
// Padded on the raw values, coloured afterwards: an ANSI escape counts
|
|
1620
1624
|
// toward String.length and would skew every column right of it.
|
|
1621
|
-
const columns = ['HOST', 'PATH', 'KIND', 'ROUTE', 'TLS', 'HTTP3', 'CURRENT', 'OPPOSITE'];
|
|
1625
|
+
const columns = ['#', 'HOST', 'PATH', 'KIND', 'ROUTE', 'TLS', 'HTTP3', 'CURRENT', 'OPPOSITE'];
|
|
1622
1626
|
const deploymentStatus = (status, includeServing = false) => {
|
|
1623
1627
|
if (!status) return `unrouted - missing${includeServing ? ' not-serving' : ''}`;
|
|
1624
1628
|
return [
|
|
@@ -1628,10 +1632,11 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
1628
1632
|
...(includeServing ? [status.serving ? 'serving' : 'not-serving'] : []),
|
|
1629
1633
|
].join(' ');
|
|
1630
1634
|
};
|
|
1631
|
-
const cellOf = (row) => {
|
|
1635
|
+
const cellOf = (row, index) => {
|
|
1632
1636
|
const facts = ingressFacts[row.host] || {};
|
|
1633
1637
|
const pathStatus = row.probes.map((probe) => `${probe.path} [${probe.statuses.join('→')}]`).join(' ');
|
|
1634
1638
|
return [
|
|
1639
|
+
`${index + 1}(${row.probes.length})`,
|
|
1635
1640
|
row.host,
|
|
1636
1641
|
pathStatus,
|
|
1637
1642
|
row.kind,
|
|
@@ -1686,7 +1691,8 @@ echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com
|
|
|
1686
1691
|
for (const cell of cells) console.log(line(cell, true));
|
|
1687
1692
|
};
|
|
1688
1693
|
for (const env of envs) {
|
|
1689
|
-
const
|
|
1694
|
+
const envRows = reportRows.filter((row) => row.env === env);
|
|
1695
|
+
const envCells = envRows.map((row, index) => cellOf(row, index));
|
|
1690
1696
|
if (envCells.length > 0) printTable(envCells, `[${env.toUpperCase()}]`);
|
|
1691
1697
|
}
|
|
1692
1698
|
console.log('');
|
|
@@ -1971,22 +1977,33 @@ EOF
|
|
|
1971
1977
|
|
|
1972
1978
|
/**
|
|
1973
1979
|
* @method instance
|
|
1974
|
-
* @
|
|
1980
|
+
* @description Deploys the custom instances a deploy declares in `conf.instances.json`.
|
|
1981
|
+
*
|
|
1982
|
+
* Every input is a flag: `--deploy-id`, `--instance-id`, `--replicas`, `--node-name`.
|
|
1983
|
+
* `--instance-id` naming a template id selects its whole variant family.
|
|
1984
|
+
* @param {string} path - Unused; every input is a flag.
|
|
1975
1985
|
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
1976
1986
|
* @memberof UnderpostRun
|
|
1977
1987
|
*/
|
|
1978
1988
|
instance: async (path = '', options = DEFAULT_OPTION) => {
|
|
1979
|
-
const env = options
|
|
1989
|
+
const env = deployEnvFactory(options);
|
|
1980
1990
|
options = {
|
|
1981
1991
|
...options,
|
|
1982
1992
|
gatewayApi: gatewayApiEnabledFactory(options),
|
|
1983
1993
|
namespace: options.namespace || 'default',
|
|
1984
1994
|
};
|
|
1985
|
-
const baseCommand =
|
|
1995
|
+
const baseCommand = cli('underpost', { local: true });
|
|
1986
1996
|
const baseClusterCommand = options.dev ? ' --dev' : '';
|
|
1987
|
-
|
|
1988
|
-
|
|
1997
|
+
const deployId = options.deployId;
|
|
1998
|
+
const id = options.instanceId;
|
|
1999
|
+
const replicas = options.replicas || 1;
|
|
2000
|
+
if (!deployId) throw new Error('The --deploy-id option is required by the instance runner');
|
|
2001
|
+
if (!id) throw new Error('The --instance-id option is required by the instance runner');
|
|
1989
2002
|
const confInstances = selectConfInstances(loadConfInstances(deployId), id);
|
|
2003
|
+
// Retarget `underpost-config` at this run's environment before any instance
|
|
2004
|
+
// Deployment is submitted: it is what `underpost host load` reads inside the pod,
|
|
2005
|
+
// and a stale one makes the container resolve the wrong NODE_ENV.
|
|
2006
|
+
if (!options.expose) Underpost.host.apply(domainContextFactory({ env, namespace: options.namespace }));
|
|
1990
2007
|
const { liveTrafficById, targetTrafficById, serving } = instanceTrafficPlanFactory({
|
|
1991
2008
|
instances: confInstances,
|
|
1992
2009
|
requestedTraffic: options.traffic,
|
|
@@ -2082,7 +2099,6 @@ EOF
|
|
|
2082
2099
|
const targetTraffic = targetTrafficById[instance.id];
|
|
2083
2100
|
const podId = `${_deployId}-${env}-${targetTraffic}`;
|
|
2084
2101
|
const ignorePods = Underpost.kubectl.get(podId, 'pods', options.namespace).map((p) => p.NAME);
|
|
2085
|
-
Underpost.deploy.configMap(env, options.namespace);
|
|
2086
2102
|
shellExec(`kubectl delete service ${podId}-service --namespace ${options.namespace} --ignore-not-found`);
|
|
2087
2103
|
shellExec(`kubectl delete deployment ${podId} --namespace ${options.namespace} --ignore-not-found`);
|
|
2088
2104
|
for (const _volume of _volumes)
|
|
@@ -2212,11 +2228,10 @@ EOF
|
|
|
2212
2228
|
|
|
2213
2229
|
/**
|
|
2214
2230
|
* @method deploy-key
|
|
2215
|
-
* @description Copies the deploy key for a specific user
|
|
2231
|
+
* @description Copies the cluster scoped deploy key for a specific user to a temporary location on the local machine.
|
|
2216
2232
|
* @param {string} path - The input value, identifier, or path for the operation (not used in this method).
|
|
2217
2233
|
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
2218
2234
|
* @param {string} options.user - The user for which to copy the deploy key.
|
|
2219
|
-
* @param {string} options.deployId - The deployment identifier associated with the deploy key.
|
|
2220
2235
|
* @memberof UnderpostRun
|
|
2221
2236
|
*/
|
|
2222
2237
|
'deploy-key': (path, options = DEFAULT_OPTION) => {
|
|
@@ -2225,13 +2240,18 @@ EOF
|
|
|
2225
2240
|
shellExec(`rm -rf /home/dd/tmp/${prefix}_*`);
|
|
2226
2241
|
return;
|
|
2227
2242
|
}
|
|
2228
|
-
if (!options.user
|
|
2229
|
-
logger.error('
|
|
2243
|
+
if (!options.user) {
|
|
2244
|
+
logger.error('The --user option is required to copy the deploy key.');
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
const sourcePath = `${Underpost.ssh.userKeyDir(options.user)}/id_rsa`;
|
|
2248
|
+
if (!fs.existsSync(sourcePath)) {
|
|
2249
|
+
logger.error(`No deploy key found at ${sourcePath}; run: node bin ssh --user ${options.user} --user-add`);
|
|
2230
2250
|
return;
|
|
2231
2251
|
}
|
|
2232
2252
|
const targetPath = `/home/dd/tmp/${prefix}_${s4()}${s4()}`;
|
|
2233
2253
|
fs.mkdirSync('/home/dd/tmp', { recursive: true });
|
|
2234
|
-
fs.copyFileSync(
|
|
2254
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
2235
2255
|
logger.info(`Copied deploy key to ${targetPath}`);
|
|
2236
2256
|
if (options.copy) pbcopy(targetPath);
|
|
2237
2257
|
},
|
|
@@ -2562,11 +2582,11 @@ EOF
|
|
|
2562
2582
|
// mode this build ran. The default/template instance owns the committed
|
|
2563
2583
|
// source files, so only its current-mode file is idempotently refreshed.
|
|
2564
2584
|
if (instance.templateId) {
|
|
2565
|
-
const instanceEnvDir =
|
|
2585
|
+
const instanceEnvDir = nodePath.dirname(instanceEnvFilePath(deployId, _id, env));
|
|
2566
2586
|
fs.mkdirpSync(instanceEnvDir);
|
|
2567
2587
|
const envsToWrite = isDefaultInstance ? [env] : ['development', 'production'];
|
|
2568
2588
|
for (const targetEnv of envsToWrite) {
|
|
2569
|
-
const templateEnvPath =
|
|
2589
|
+
const templateEnvPath = instanceEnvFilePath(deployId, instance.templateId, targetEnv);
|
|
2570
2590
|
if (!fs.existsSync(templateEnvPath))
|
|
2571
2591
|
throw new Error(`[instance-build-manifest] Missing canonical env file: ${templateEnvPath}`);
|
|
2572
2592
|
const baseEnv = dotenv.parse(fs.readFileSync(templateEnvPath, 'utf8'));
|
|
@@ -2578,7 +2598,7 @@ EOF
|
|
|
2578
2598
|
containerDeployId: `${_deployId}-${targetEnv}`,
|
|
2579
2599
|
builders: instanceEnvBuilder ? { [deployId]: instanceEnvBuilder } : {},
|
|
2580
2600
|
});
|
|
2581
|
-
writeEnv(
|
|
2601
|
+
writeEnv(instanceEnvFilePath(deployId, _id, targetEnv), builtEnv);
|
|
2582
2602
|
}
|
|
2583
2603
|
logger.info('[instance-build-manifest] Instance env written', {
|
|
2584
2604
|
dir: instanceEnvDir,
|
|
@@ -2660,7 +2680,6 @@ EOF
|
|
|
2660
2680
|
* @memberof UnderpostRun
|
|
2661
2681
|
*/
|
|
2662
2682
|
'host-update': async (path, options = DEFAULT_OPTION) => {
|
|
2663
|
-
// const baseCommand = options.dev ? 'node bin' : 'underpost';
|
|
2664
2683
|
shellExec(`chmod +x ${options.underpostRoot}/scripts/rocky-setup.sh`);
|
|
2665
2684
|
shellExec(`${options.underpostRoot}/scripts/rocky-setup.sh${options.dev ? ' --install-dev' : ``}`);
|
|
2666
2685
|
},
|
|
@@ -2727,7 +2746,7 @@ EOF`);
|
|
|
2727
2746
|
* @memberof UnderpostRun
|
|
2728
2747
|
*/
|
|
2729
2748
|
'dd-container': async (path = '', options = DEFAULT_OPTION) => {
|
|
2730
|
-
const baseCommand =
|
|
2749
|
+
const baseCommand = cli('underpost', { local: true });
|
|
2731
2750
|
const baseClusterCommand = options.dev ? ' --dev' : '';
|
|
2732
2751
|
const currentImage = options.imageName
|
|
2733
2752
|
? options.imageName
|
|
@@ -2824,15 +2843,15 @@ EOF`);
|
|
|
2824
2843
|
* @memberof UnderpostRun
|
|
2825
2844
|
*/
|
|
2826
2845
|
'git-conf': (path = '', options = DEFAULT_OPTION) => {
|
|
2827
|
-
const defaultUsername = Underpost.
|
|
2828
|
-
const defaultEmail = Underpost.
|
|
2846
|
+
const defaultUsername = Underpost.host.store.get('GITHUB_USERNAME');
|
|
2847
|
+
const defaultEmail = Underpost.host.store.get('GITHUB_EMAIL');
|
|
2829
2848
|
const validPath = path && path.split(',').length;
|
|
2830
2849
|
const [username, email] = validPath ? path.split(',') : [defaultUsername, defaultEmail];
|
|
2831
2850
|
if (validPath) {
|
|
2832
|
-
Underpost.
|
|
2833
|
-
Underpost.
|
|
2834
|
-
Underpost.
|
|
2835
|
-
Underpost.
|
|
2851
|
+
Underpost.host.store.set('GITHUB_USERNAME', username);
|
|
2852
|
+
Underpost.host.store.set('GITHUB_EMAIL', email);
|
|
2853
|
+
Underpost.host.store.get('GITHUB_USERNAME');
|
|
2854
|
+
Underpost.host.store.get('GITHUB_EMAIL');
|
|
2836
2855
|
}
|
|
2837
2856
|
shellExec(
|
|
2838
2857
|
`git config --global credential.helper "" && ` +
|
|
@@ -2863,7 +2882,7 @@ EOF`);
|
|
|
2863
2882
|
|
|
2864
2883
|
/**
|
|
2865
2884
|
* @method promote
|
|
2866
|
-
* @description Switches traffic between blue/green deployments for a specified deployment ID(s) (uses `dd.
|
|
2885
|
+
* @description Switches traffic between blue/green deployments for a specified deployment ID(s) (uses `dd.routes` for 'dd', or a specific ID).
|
|
2867
2886
|
* When `--tls` is set, rebuilds the proxy manifest with `--cert` so the HTTPProxy includes
|
|
2868
2887
|
* TLS config, deletes stale Certificate resources, then reapplies the proxy and secret.yaml
|
|
2869
2888
|
* (cert-manager Certificate resources) for each affected deployment.
|
|
@@ -2899,7 +2918,7 @@ EOF`);
|
|
|
2899
2918
|
};
|
|
2900
2919
|
|
|
2901
2920
|
if (inputDeployId === 'dd') {
|
|
2902
|
-
for (const deployId of
|
|
2921
|
+
for (const deployId of readDeployRoutes()) {
|
|
2903
2922
|
const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, {
|
|
2904
2923
|
namespace: options.namespace,
|
|
2905
2924
|
env: inputEnv,
|
|
@@ -2925,44 +2944,6 @@ EOF`);
|
|
|
2925
2944
|
applyCerts(inputDeployId, targetTraffic);
|
|
2926
2945
|
}
|
|
2927
2946
|
},
|
|
2928
|
-
/**
|
|
2929
|
-
* @method metrics
|
|
2930
|
-
* @description Deploys Prometheus and Grafana for metrics monitoring, targeting the hosts defined in the deployment configuration files.
|
|
2931
|
-
* @param {string} path - The input value, identifier, or path for the operation.
|
|
2932
|
-
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
2933
|
-
* @memberof UnderpostRun
|
|
2934
|
-
*/
|
|
2935
|
-
metrics: async (path, options = DEFAULT_OPTION) => {
|
|
2936
|
-
if (path === 'server') {
|
|
2937
|
-
shellExec(
|
|
2938
|
-
`kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/high-availability-1.21+.yaml`,
|
|
2939
|
-
);
|
|
2940
|
-
await timer(2000);
|
|
2941
|
-
|
|
2942
|
-
shellExec(`kubectl patch deployment metrics-server -n kube-system \
|
|
2943
|
-
--type='json' \
|
|
2944
|
-
-p='[
|
|
2945
|
-
{
|
|
2946
|
-
"op":"add",
|
|
2947
|
-
"path":"/spec/template/spec/containers/0/args/-",
|
|
2948
|
-
"value":"--kubelet-insecure-tls"
|
|
2949
|
-
}
|
|
2950
|
-
]'`);
|
|
2951
|
-
shellExec(`kubectl scale deployment metrics-server \
|
|
2952
|
-
-n kube-system \
|
|
2953
|
-
--replicas=1`);
|
|
2954
|
-
|
|
2955
|
-
return;
|
|
2956
|
-
}
|
|
2957
|
-
const deployList = fs.readFileSync(`./engine-private/deploy/dd.router`, 'utf8').split(',');
|
|
2958
|
-
let hosts = [];
|
|
2959
|
-
for (const deployId of deployList) {
|
|
2960
|
-
const confServer = loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`);
|
|
2961
|
-
hosts = hosts.concat(Object.keys(confServer));
|
|
2962
|
-
}
|
|
2963
|
-
shellExec(`node bin cluster --prom ${hosts.join(',')}`);
|
|
2964
|
-
shellExec(`node bin cluster --grafana`);
|
|
2965
|
-
},
|
|
2966
2947
|
/**
|
|
2967
2948
|
* @method cluster
|
|
2968
2949
|
* @description Deploys a full production/development ready Kubernetes cluster environment including MongoDB,
|
|
@@ -2975,21 +2956,24 @@ EOF`);
|
|
|
2975
2956
|
* references, and every host is written to `/etc/hosts` so the operator's browser reaches the PWA at
|
|
2976
2957
|
* `https://<host>` on the local machine.
|
|
2977
2958
|
*
|
|
2978
|
-
* Custom instances
|
|
2959
|
+
* Custom instances come from `--instance-id`. They are resolved per deploy against that deploy's
|
|
2979
2960
|
* own `conf.instances.json`, so an id only runs where its deploy declares it, and each one is deployed after its
|
|
2980
2961
|
* deploy's default workload is serving — an instance reads the parent's world configuration over the parent's
|
|
2981
2962
|
* gRPC ClusterIP at boot. Instance hosts share the deploy's environment: the same self-signed certificates and
|
|
2982
2963
|
* `/etc/hosts` pass in development, the same cert-manager issuance in production.
|
|
2983
|
-
*
|
|
2984
|
-
*
|
|
2985
|
-
*
|
|
2964
|
+
*
|
|
2965
|
+
* Every input is a flag: `--runtime-image` (default `express`), `--deploy-id` (comma-separated, or the `dd` meta
|
|
2966
|
+
* id; unset reads the route table) and `--instance-id` (comma-separated; a template id such as `mmo-server`
|
|
2967
|
+
* selects its whole variant family):
|
|
2968
|
+
* `node bin run cluster --runtime-image express --deploy-id dd-cyberia --instance-id mmo-server --dev`
|
|
2969
|
+
* @param {string} path - Unused; every input is a flag.
|
|
2986
2970
|
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
2987
2971
|
* @memberof UnderpostRun
|
|
2988
2972
|
*/
|
|
2989
2973
|
cluster: async (path = '', options = DEFAULT_OPTION) => {
|
|
2990
2974
|
const { underpostRoot } = options;
|
|
2991
|
-
const env = options
|
|
2992
|
-
const baseCommand =
|
|
2975
|
+
const env = deployEnvFactory(options);
|
|
2976
|
+
const baseCommand = cli('underpost', { local: true });
|
|
2993
2977
|
const baseClusterCommand = options.dev ? ' --dev' : '';
|
|
2994
2978
|
const clusterType = clusterTypeFactory(options, 'kubeadm');
|
|
2995
2979
|
shellCd(`/home/dd/engine`);
|
|
@@ -2997,23 +2981,15 @@ EOF`);
|
|
|
2997
2981
|
await timer(5000);
|
|
2998
2982
|
shellExec(`${baseCommand} cluster${baseClusterCommand} --${clusterType}`);
|
|
2999
2983
|
await timer(5000);
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
? path.split(',')
|
|
3003
|
-
: [
|
|
3004
|
-
'express',
|
|
3005
|
-
fs.readFileSync(`${underpostRoot}/engine-private/deploy/dd.router`, 'utf8').replaceAll(',', '+'),
|
|
3006
|
-
'',
|
|
3007
|
-
];
|
|
2984
|
+
const runtimeImage = options.runtimeImage || 'express';
|
|
2985
|
+
const instanceListId = options.instanceId || '';
|
|
3008
2986
|
// shellExec(
|
|
3009
2987
|
// `${baseCommand} image${baseClusterCommand} --build ${
|
|
3010
2988
|
// runtimeImage ? ` --pull-base --path ${underpostRoot}/src/runtime/${runtimeImage}` : ''
|
|
3011
2989
|
// } --${clusterType}`,
|
|
3012
2990
|
// );
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
logger.warn('No deploy list provided');
|
|
3016
|
-
} else deployList = deployList.split('+');
|
|
2991
|
+
const deployList = options.deployId ? resolveDeployList(options.deployId) : readDeployRoutes();
|
|
2992
|
+
if (deployList.length === 0) logger.warn('No deploy list provided');
|
|
3017
2993
|
await timer(5000);
|
|
3018
2994
|
// --reset-mongodb wipes the retained hostPath volumes before the rollout.
|
|
3019
2995
|
// This workflow already tore the node down and re-imports every database
|
|
@@ -3090,10 +3066,13 @@ EOF`);
|
|
|
3090
3066
|
const hostListenResult = etcHostFactory(hosts);
|
|
3091
3067
|
logger.info(hostListenResult.renderHosts);
|
|
3092
3068
|
}
|
|
3093
|
-
const version = 'v3.
|
|
3069
|
+
const version = 'v3.3.73';
|
|
3094
3070
|
const instanceOptionsFactory = (deployId, instanceId) => ({
|
|
3095
3071
|
...options,
|
|
3096
3072
|
...clusterContextFactory(clusterType),
|
|
3073
|
+
deployId,
|
|
3074
|
+
instanceId,
|
|
3075
|
+
replicas: options.replicas || 1,
|
|
3097
3076
|
gatewayApi,
|
|
3098
3077
|
gatewayBootstrapComplete: true,
|
|
3099
3078
|
tls: true,
|
|
@@ -3140,17 +3119,16 @@ EOF`);
|
|
|
3140
3119
|
sudo rm -rf ./engine-private/, \
|
|
3141
3120
|
node bin clone underpostnet/engine-cyberia-private, \
|
|
3142
3121
|
sudo mv ./engine-cyberia-private ./engine-private, \
|
|
3143
|
-
node bin env
|
|
3144
|
-
node ./engine-private/itc-scripts/dd-cyberia-0.js, \
|
|
3122
|
+
node bin app load --env ${env} --args deploy-id=dd-cyberia, \
|
|
3145
3123
|
sudo chown -R dd:dd /home/dd/engine/src/client/public/cyberia, \
|
|
3146
|
-
node bin env
|
|
3124
|
+
node bin app load --env ${env} --args deploy-id=dd-cyberia, \
|
|
3147
3125
|
node bin client dd-cyberia ${env}, \
|
|
3148
3126
|
node bin start dd-cyberia ${env} --run'`
|
|
3149
3127
|
: '');
|
|
3150
3128
|
deployFlagsById[deployId] = deployFlags;
|
|
3151
3129
|
// SSR status and context documents belong to the ingress bootstrap, so
|
|
3152
3130
|
// build them on the host before any workload Deployment is submitted.
|
|
3153
|
-
shellExec(`${baseCommand} client ${deployId} ${env}`);
|
|
3131
|
+
shellExec(`${baseCommand} client ${deployId} --env ${env}`);
|
|
3154
3132
|
shellExec(`${baseCommand} deploy ${deployId} ${env} --build-manifest ${deployFlags}`);
|
|
3155
3133
|
// Seed the static tree before the routes exist, so every status page and
|
|
3156
3134
|
// intercepted context the manifests just pointed at resolves from the
|
|
@@ -3253,10 +3231,7 @@ EOF`);
|
|
|
3253
3231
|
// keep serving the custom fallback until the atomic promotion completes.
|
|
3254
3232
|
for (const instanceId of instancesByDeployId[deployId].ids) {
|
|
3255
3233
|
logger.info('[cluster] Deploying custom instance', { deployId, instanceId, env, clusterType });
|
|
3256
|
-
await UnderpostRun.RUNNERS.instance(
|
|
3257
|
-
`${deployId},${instanceId},${options.replicas || 1}`,
|
|
3258
|
-
instanceOptionsFactory(deployId, instanceId),
|
|
3259
|
-
);
|
|
3234
|
+
await UnderpostRun.RUNNERS.instance('', instanceOptionsFactory(deployId, instanceId));
|
|
3260
3235
|
}
|
|
3261
3236
|
}
|
|
3262
3237
|
logger.info('[cluster] Ingress stack deployed', {
|
|
@@ -3648,6 +3623,10 @@ EOF`);
|
|
|
3648
3623
|
/**
|
|
3649
3624
|
* @method deploy
|
|
3650
3625
|
* @description Deploys a specified service (identified by `path`) using blue/green strategy, monitors its status, and switches traffic upon readiness.
|
|
3626
|
+
*
|
|
3627
|
+
* The target colour is restarted when it is already deployed, and built and applied when it is not:
|
|
3628
|
+
* between cycles only the routed colour has a Deployment, so its counterpart being absent is the
|
|
3629
|
+
* normal state rather than a precondition.
|
|
3651
3630
|
* @param {string} path - The input value, identifier, or path for the operation (used as the deployment ID to deploy).
|
|
3652
3631
|
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
3653
3632
|
* @memberof UnderpostRun
|
|
@@ -3655,19 +3634,57 @@ EOF`);
|
|
|
3655
3634
|
deploy: async (path, options = DEFAULT_OPTION) => {
|
|
3656
3635
|
const deployId = path;
|
|
3657
3636
|
const env = options.dev ? 'development' : 'production';
|
|
3637
|
+
const namespace = options.namespace || 'default';
|
|
3658
3638
|
const { validVersion } = Underpost.repo.privateConfUpdate(deployId);
|
|
3659
3639
|
if (!validVersion) throw new Error('Version mismatch');
|
|
3660
|
-
|
|
3661
|
-
const
|
|
3640
|
+
options = { ...options, gatewayApi: gatewayApiEnabledFactory(options) };
|
|
3641
|
+
const currentTraffic = Underpost.deploy.getCurrentTraffic(deployId, {
|
|
3642
|
+
namespace,
|
|
3643
|
+
env,
|
|
3644
|
+
gatewayApi: options.gatewayApi,
|
|
3645
|
+
});
|
|
3646
|
+
const { targetTraffic, create } = redeployPlanFactory({
|
|
3647
|
+
liveTraffic: currentTraffic,
|
|
3648
|
+
hasDeployment: (colour) =>
|
|
3649
|
+
Underpost.deploy.deploymentExists({ deployment: `${deployId}-${env}-${colour}`, namespace }),
|
|
3650
|
+
});
|
|
3662
3651
|
const ignorePods = Underpost.kubectl
|
|
3663
|
-
.get(`${deployId}-${env}-${targetTraffic}`, 'pods',
|
|
3652
|
+
.get(`${deployId}-${env}-${targetTraffic}`, 'pods', namespace)
|
|
3664
3653
|
.map((p) => p.NAME);
|
|
3665
3654
|
|
|
3666
|
-
|
|
3655
|
+
if (create) {
|
|
3656
|
+
// The target colour having no Deployment is the steady state of a
|
|
3657
|
+
// blue/green cycle, not a missing prerequisite: build that colour's
|
|
3658
|
+
// manifest and apply it, leaving the live colour routed until the
|
|
3659
|
+
// promotion below.
|
|
3660
|
+
const replicas = options.replicas || 1;
|
|
3661
|
+
const node =
|
|
3662
|
+
(currentTraffic
|
|
3663
|
+
? Underpost.deploy.deploymentNode({ deployment: `${deployId}-${env}-${currentTraffic}`, namespace })
|
|
3664
|
+
: '') || options.nodeName;
|
|
3665
|
+
logger.info('Target traffic colour is not deployed; creating it', {
|
|
3666
|
+
deployId,
|
|
3667
|
+
env,
|
|
3668
|
+
targetTraffic,
|
|
3669
|
+
node: node || '(unpinned)',
|
|
3670
|
+
});
|
|
3671
|
+
const deployFlags =
|
|
3672
|
+
` --versions ${targetTraffic} --replicas ${replicas} --namespace ${namespace}${node ? ` --node ${node}` : ''}` +
|
|
3673
|
+
`${options.image ? ` --image ${options.image}` : ''}` +
|
|
3674
|
+
`${Underpost.deploy.timeoutFlagsFactory(options)}${options.imagePullPolicy ? ` --image-pull-policy ${options.imagePullPolicy}` : ''}` +
|
|
3675
|
+
`${Underpost.deploy.gatewayApiFlagsFactory(options)}`;
|
|
3676
|
+
// `--build-manifest` returns before any cluster mutation, so the colour is
|
|
3677
|
+
// rendered first and applied by the second call. `--traffic` keeps the
|
|
3678
|
+
// generated traffic Service on the colour still serving.
|
|
3679
|
+
shellExec(
|
|
3680
|
+
`node bin deploy --build-manifest --info-router --traffic ${currentTraffic || targetTraffic}${deployFlags} ${deployId} ${env}`,
|
|
3681
|
+
);
|
|
3682
|
+
shellExec(`node bin deploy --disable-update-proxy${deployFlags} ${deployId} ${env}`);
|
|
3683
|
+
} else shellExec(`sudo kubectl rollout restart deployment/${deployId}-${env}-${targetTraffic} -n ${namespace}`);
|
|
3667
3684
|
|
|
3668
|
-
await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods,
|
|
3685
|
+
await Underpost.monitor.monitorReadyRunner(deployId, env, targetTraffic, ignorePods, namespace);
|
|
3669
3686
|
|
|
3670
|
-
Underpost.deploy.switchTraffic(deployId, env, targetTraffic, options.replicas,
|
|
3687
|
+
Underpost.deploy.switchTraffic(deployId, env, targetTraffic, options.replicas, namespace, options);
|
|
3671
3688
|
},
|
|
3672
3689
|
|
|
3673
3690
|
/**
|
|
@@ -3757,7 +3774,10 @@ EOF`);
|
|
|
3757
3774
|
}`;
|
|
3758
3775
|
shellExec(cmd, { async: true });
|
|
3759
3776
|
}
|
|
3760
|
-
|
|
3777
|
+
// A bare `return` here exited 0, so a runtime that latched `container-status=error` looked
|
|
3778
|
+
// like a clean start to everything upstream — the shell, and any CI job wrapping it.
|
|
3779
|
+
if ((await awaitDeployMonitor()) !== true)
|
|
3780
|
+
throw new Error(`[run dev] ${deployId} api runtime reported ${RUNTIME_STATUS.ERROR}; see the logs above`);
|
|
3761
3781
|
{
|
|
3762
3782
|
const cmd = `npm run dev:client ${deployId} ${subConf} ${host} ${_path} proxy${options.tls ? ' tls' : ''}`;
|
|
3763
3783
|
|
|
@@ -3765,7 +3785,8 @@ EOF`);
|
|
|
3765
3785
|
async: true,
|
|
3766
3786
|
});
|
|
3767
3787
|
}
|
|
3768
|
-
if ((await awaitDeployMonitor()) !== true)
|
|
3788
|
+
if ((await awaitDeployMonitor()) !== true)
|
|
3789
|
+
throw new Error(`[run dev] ${deployId} client runtime reported ${RUNTIME_STATUS.ERROR}; see the logs above`);
|
|
3769
3790
|
shellExec(
|
|
3770
3791
|
`NODE_ENV=development node src/proxy proxy ${deployId} ${subConf} ${host} ${_path}${options.tls ? ' tls' : ''}`,
|
|
3771
3792
|
);
|
|
@@ -3780,7 +3801,7 @@ EOF`);
|
|
|
3780
3801
|
*/
|
|
3781
3802
|
service: async (path = '', options = DEFAULT_OPTION) => {
|
|
3782
3803
|
const env = options.dev ? 'development' : 'production';
|
|
3783
|
-
const baseCommand =
|
|
3804
|
+
const baseCommand = cli('underpost', { local: true });
|
|
3784
3805
|
const baseClusterCommand = options.dev ? ' --dev' : '';
|
|
3785
3806
|
shellCd(`/home/dd/engine`);
|
|
3786
3807
|
let [deployId, serviceId, host, _path, replicas, image, node] = path.split(',');
|
|
@@ -3885,26 +3906,6 @@ EOF`);
|
|
|
3885
3906
|
logger.info(hostListenResult.renderHosts);
|
|
3886
3907
|
},
|
|
3887
3908
|
|
|
3888
|
-
/**
|
|
3889
|
-
* @method sh
|
|
3890
|
-
* @description Enables remote control for the Kitty terminal emulator.
|
|
3891
|
-
* @param {string} path - The input value, identifier, or path for the operation.
|
|
3892
|
-
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
3893
|
-
* @memberof UnderpostRun
|
|
3894
|
-
*/
|
|
3895
|
-
sh: async (path = '', options = DEFAULT_OPTION) => {
|
|
3896
|
-
let [operator, arg0, arg1] = path.split(',');
|
|
3897
|
-
if (operator == 'copy') {
|
|
3898
|
-
shellExec(
|
|
3899
|
-
`kitty @ get-text ${arg0 === 'all' ? '--match all' : '--self'} --extent all${
|
|
3900
|
-
arg1 === 'ansi' ? ' --ansi yes' : ''
|
|
3901
|
-
} | kitty +kitten clipboard`,
|
|
3902
|
-
);
|
|
3903
|
-
return;
|
|
3904
|
-
}
|
|
3905
|
-
shellExec(`kitty -o allow_remote_control=yes`);
|
|
3906
|
-
},
|
|
3907
|
-
|
|
3908
3909
|
/**
|
|
3909
3910
|
* @method log
|
|
3910
3911
|
* @description Searches and highlights keywords in a specified log file, optionally showing surrounding lines.
|
|
@@ -3963,28 +3964,44 @@ EOF`);
|
|
|
3963
3964
|
|
|
3964
3965
|
// Services
|
|
3965
3966
|
logger.info('Process info');
|
|
3966
|
-
shellExec(`sudo ps -p ${pid} -o pid,ppid,user,stime,etime,cmd
|
|
3967
|
+
shellExec(`sudo ps -p ${pid} -o pid,ppid,user,stime,etime,cmd`, {
|
|
3968
|
+
silentOnError: true,
|
|
3969
|
+
});
|
|
3967
3970
|
logger.info('Command line');
|
|
3968
|
-
shellExec(`sudo cat /proc/${pid}/cmdline | tr '\\0' ' ' ; echo
|
|
3971
|
+
shellExec(`sudo cat /proc/${pid}/cmdline | tr '\\0' ' ' ; echo`, {
|
|
3972
|
+
silentOnError: true,
|
|
3973
|
+
});
|
|
3969
3974
|
logger.info('Executable path');
|
|
3970
3975
|
shellExec(`sudo readlink -f /proc/${pid}/exe`);
|
|
3971
3976
|
logger.info('Working directory');
|
|
3972
|
-
shellExec(`sudo readlink -f /proc/${pid}/cwd
|
|
3977
|
+
shellExec(`sudo readlink -f /proc/${pid}/cwd`, {
|
|
3978
|
+
silentOnError: true,
|
|
3979
|
+
});
|
|
3973
3980
|
logger.info('Environment variables (first 200)');
|
|
3974
|
-
shellExec(`sudo tr '\\0' '\\n' </proc/${pid}/environ | head -200
|
|
3981
|
+
shellExec(`sudo tr '\\0' '\\n' </proc/${pid}/environ | head -200`, {
|
|
3982
|
+
silentOnError: true,
|
|
3983
|
+
});
|
|
3975
3984
|
|
|
3976
3985
|
// Parent
|
|
3977
3986
|
logger.info('Parent process');
|
|
3978
|
-
const parentInfo = shellExec(`sudo ps -o pid,ppid,user,cmd -p ${pid}`, {
|
|
3987
|
+
const parentInfo = shellExec(`sudo ps -o pid,ppid,user,cmd -p ${pid}`, {
|
|
3988
|
+
stdout: true,
|
|
3989
|
+
silent: true,
|
|
3990
|
+
silentOnError: true,
|
|
3991
|
+
});
|
|
3979
3992
|
console.log(parentInfo);
|
|
3980
3993
|
const ppidMatch = parentInfo.split('\n').find((l) => l.trim().startsWith(pid));
|
|
3981
3994
|
if (ppidMatch) {
|
|
3982
3995
|
const ppid = ppidMatch.trim().split(/\s+/)[1];
|
|
3983
3996
|
logger.info(`Parent PID: ${ppid}`);
|
|
3984
|
-
shellExec(`ps -fp ${ppid}
|
|
3997
|
+
shellExec(`ps -fp ${ppid}`, {
|
|
3998
|
+
silentOnError: true,
|
|
3999
|
+
});
|
|
3985
4000
|
}
|
|
3986
4001
|
logger.info('Process tree');
|
|
3987
|
-
shellExec(`pstree -s ${pid}
|
|
4002
|
+
shellExec(`pstree -s ${pid}`, {
|
|
4003
|
+
silentOnError: true,
|
|
4004
|
+
});
|
|
3988
4005
|
},
|
|
3989
4006
|
|
|
3990
4007
|
/**
|
|
@@ -4029,7 +4046,7 @@ EOF`);
|
|
|
4029
4046
|
'deploy-test': async (path, options = DEFAULT_OPTION) => {
|
|
4030
4047
|
// Note: use recomendation empty deploy cluster: node bin --dev cluster
|
|
4031
4048
|
const env = options.dev ? 'development' : 'production';
|
|
4032
|
-
const baseCommand =
|
|
4049
|
+
const baseCommand = cli('underpost', { local: true });
|
|
4033
4050
|
const baseClusterCommand = options.dev ? ' --dev' : '';
|
|
4034
4051
|
const inputs = path ? path.split(',') : [];
|
|
4035
4052
|
const deployId = inputs[0] ? inputs[0] : 'dd-test';
|
|
@@ -4038,10 +4055,12 @@ EOF`);
|
|
|
4038
4055
|
: [
|
|
4039
4056
|
`npm install -g npm@11.2.0`,
|
|
4040
4057
|
`npm install -g underpost`,
|
|
4041
|
-
`${baseCommand}
|
|
4058
|
+
`${baseCommand} host load`,
|
|
4042
4059
|
`${baseCommand} start --build --run ${deployId} ${env}`,
|
|
4043
4060
|
];
|
|
4044
|
-
shellExec(
|
|
4061
|
+
shellExec(
|
|
4062
|
+
`node bin run sync${baseClusterCommand} --deploy-id-cron-jobs none --deploy-id dd-test --cmd ${shellArgumentFactory(cmd)}`,
|
|
4063
|
+
);
|
|
4045
4064
|
},
|
|
4046
4065
|
|
|
4047
4066
|
/**
|
|
@@ -4172,26 +4191,7 @@ EOF`);
|
|
|
4172
4191
|
|
|
4173
4192
|
shellCd('/home/dd/engine');
|
|
4174
4193
|
},
|
|
4175
|
-
|
|
4176
|
-
* @method pull-rocky-image
|
|
4177
|
-
* @description Pulls the base `rockylinux:9` image from Docker Hub via Podman.
|
|
4178
|
-
* @param {string} path - The input value, identifier, or path for the operation.
|
|
4179
|
-
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
4180
|
-
* @memberof UnderpostRun
|
|
4181
|
-
*/
|
|
4182
|
-
'pull-rocky-image': (path, options = DEFAULT_OPTION) => {
|
|
4183
|
-
shellExec(`sudo podman pull docker.io/library/rockylinux:9`);
|
|
4184
|
-
},
|
|
4185
|
-
/**
|
|
4186
|
-
* @method rmi
|
|
4187
|
-
* @description Forces the removal of all local Podman images (`podman rmi $(podman images -qa) --force`).
|
|
4188
|
-
* @param {string} path - The input value, identifier, or path for the operation.
|
|
4189
|
-
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
4190
|
-
* @memberof UnderpostRun
|
|
4191
|
-
*/
|
|
4192
|
-
rmi: (path, options = DEFAULT_OPTION) => {
|
|
4193
|
-
shellExec(`podman rmi $(podman images -qa) --force`);
|
|
4194
|
-
},
|
|
4194
|
+
|
|
4195
4195
|
/**
|
|
4196
4196
|
* @method kill
|
|
4197
4197
|
* @description Kills processes listening on the specified port(s). If the `path` contains a `+`, it treats it as a range of ports to kill.
|
|
@@ -4237,333 +4237,6 @@ EOF`);
|
|
|
4237
4237
|
if (options.copy) pbcopy(password);
|
|
4238
4238
|
else console.log(password);
|
|
4239
4239
|
},
|
|
4240
|
-
/**
|
|
4241
|
-
* @method sops-setup
|
|
4242
|
-
* @description End-to-end SOPS/Age onboarding for a host: installs tooling, generates the Age
|
|
4243
|
-
* keypair and creation rules, pins the key path for non-interactive runs, encrypts the
|
|
4244
|
-
* requested Secrets into the Git-tracked store, then validates and applies them.
|
|
4245
|
-
*
|
|
4246
|
-
* Every step is idempotent and re-runnable. Notably it delegates key generation to
|
|
4247
|
-
* `secret sops --init` rather than calling `age-keygen` directly: a bare `age-keygen -o`
|
|
4248
|
-
* overwrites an existing key, which would orphan every manifest already encrypted to the
|
|
4249
|
-
* previous recipient with no way to recover them.
|
|
4250
|
-
*
|
|
4251
|
-
* On a host that pulled a store created elsewhere, the freshly generated key is not a recipient
|
|
4252
|
-
* of the inherited manifests. `init()` registers this host in the creation rules so what it
|
|
4253
|
-
* encrypts from here on stays readable, but existing manifests can only be re-keyed from a host
|
|
4254
|
-
* that still holds a decrypting key. That case is reported per secret and then raised by the
|
|
4255
|
-
* apply pre-flight with the available remedies, rather than surfacing as a sops decrypt error.
|
|
4256
|
-
*
|
|
4257
|
-
* Onboards the whole self-hosted data tier by default — PostgreSQL, MariaDB, and MongoDB
|
|
4258
|
-
* (`postgres-secret`, `mariadb-secret`, `mongodb-secret`, `mongodb-keyfile`). The MongoDB
|
|
4259
|
-
* keyfile is included because the StatefulSet mounts it for intra-replica-set auth and will
|
|
4260
|
-
* not start without it. Pass an explicit comma-separated list to narrow the set.
|
|
4261
|
-
*
|
|
4262
|
-
* Secret values are resolved per data key, in order:
|
|
4263
|
-
* 1. the origin seed file, when one exists (`engine-private/postgresql-password`) — this is
|
|
4264
|
-
* the real onboarding path, carrying the credential the cluster already runs on;
|
|
4265
|
-
* 2. `--args` as `key=value` pairs, for a value supplied by the operator;
|
|
4266
|
-
* 3. a freshly generated value: a base64 keyfile for `mongodb-keyfile`, `admin` for a
|
|
4267
|
-
* `username`, otherwise a 24-character secure password.
|
|
4268
|
-
*
|
|
4269
|
-
* Plaintext manifests are written by Node under `/dev/shm` at mode 600 and shredded by
|
|
4270
|
-
* `encrypt()`. They are never emitted through a shell heredoc, which would place the
|
|
4271
|
-
* credential in the command string and therefore in the process table and the command log.
|
|
4272
|
-
*
|
|
4273
|
-
* Usage:
|
|
4274
|
-
* underpost run sops-setup # postgres + mariadb + mongo
|
|
4275
|
-
* underpost run sops-setup mongodb-secret,mongodb-keyfile --namespace prod
|
|
4276
|
-
* underpost run sops-setup postgres-secret --args "password=s3cr3t"
|
|
4277
|
-
* underpost run sops-setup --dry-run # stop before mutating cluster
|
|
4278
|
-
* underpost run sops-setup --force # replace stored manifests
|
|
4279
|
-
* @param {string} path - Comma-separated Secret names to onboard. Defaults to the full data
|
|
4280
|
-
* tier: postgres-secret, mariadb-secret, mongodb-secret, mongodb-keyfile.
|
|
4281
|
-
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
4282
|
-
* @param {string} options.namespace - Target namespace for the store and the apply (default: 'default').
|
|
4283
|
-
* @param {string} options.args - Comma-separated `key=value` overrides for Secret data keys.
|
|
4284
|
-
* @param {boolean} options.dryRun - Validate and server-dry-run only; never apply.
|
|
4285
|
-
* @param {boolean} options.force - Replace encrypted manifests that already exist.
|
|
4286
|
-
* @memberof UnderpostRun
|
|
4287
|
-
*/
|
|
4288
|
-
'sops-setup': (path = '', options = DEFAULT_OPTION) => {
|
|
4289
|
-
const namespace = options.namespace || 'default';
|
|
4290
|
-
const secretNames = (path || SOPS_SETUP_DEFAULT_SECRETS.join(','))
|
|
4291
|
-
.split(',')
|
|
4292
|
-
.map((name) => name.trim())
|
|
4293
|
-
.filter(Boolean);
|
|
4294
|
-
|
|
4295
|
-
// `--args key=value,key2=value2` overrides, applied to any secret that declares that key.
|
|
4296
|
-
const overrides = `${options.args || ''}`.split(',').reduce((acc, pair) => {
|
|
4297
|
-
const separator = pair.indexOf('=');
|
|
4298
|
-
if (separator > 0) acc[pair.slice(0, separator).trim()] = pair.slice(separator + 1).trim();
|
|
4299
|
-
return acc;
|
|
4300
|
-
}, {});
|
|
4301
|
-
|
|
4302
|
-
logger.info('sops-setup', { secretNames, namespace, dryRun: !!options.dryRun, force: !!options.force });
|
|
4303
|
-
|
|
4304
|
-
// 1. Host tooling, then keypair + creation rules. Both no-op when already present.
|
|
4305
|
-
Underpost.secret.sops.installTooling();
|
|
4306
|
-
Underpost.secret.sops.init();
|
|
4307
|
-
|
|
4308
|
-
// 2. Pin the resolved key path for non-interactive runs (systemd units, CronJobs, sudo).
|
|
4309
|
-
// Written with the concrete path rather than a guessed default, because `sudo` resets
|
|
4310
|
-
// HOME and a wrong guess surfaces later as an opaque decrypt failure.
|
|
4311
|
-
const keyFile = Underpost.secret.sops.keyFile();
|
|
4312
|
-
shellExec(
|
|
4313
|
-
`sudo tee /etc/profile.d/underpost-sops.sh >/dev/null <<'UNDERPOST_SOPS_ENV_EOF'
|
|
4314
|
-
export SOPS_AGE_KEY_FILE="\${SOPS_AGE_KEY_FILE:-${keyFile}}"
|
|
4315
|
-
UNDERPOST_SOPS_ENV_EOF`,
|
|
4316
|
-
);
|
|
4317
|
-
shellExec(`sudo chmod 644 /etc/profile.d/underpost-sops.sh`);
|
|
4318
|
-
|
|
4319
|
-
// 3. Build and encrypt each requested Secret.
|
|
4320
|
-
const stageDir = '/dev/shm/underpost-secrets';
|
|
4321
|
-
const held = Underpost.secret.sops.localRecipients();
|
|
4322
|
-
fs.ensureDirSync(stageDir);
|
|
4323
|
-
fs.chmodSync(stageDir, 0o700);
|
|
4324
|
-
try {
|
|
4325
|
-
for (const name of secretNames) {
|
|
4326
|
-
const stored = Underpost.secret.sops.has(name, namespace);
|
|
4327
|
-
if (stored && !options.force) {
|
|
4328
|
-
// A stored manifest this host cannot open is present but unusable here, so reporting it
|
|
4329
|
-
// as onboarded would send the operator on to an apply that is guaranteed to fail.
|
|
4330
|
-
if (Underpost.secret.sops.decryptable(Underpost.secret.sops.manifestPath(name, namespace), held))
|
|
4331
|
-
logger.info(`${name} is already onboarded in ns/${namespace}; skipping (use --force to replace)`);
|
|
4332
|
-
else
|
|
4333
|
-
logger.warn(
|
|
4334
|
-
`${name} is stored in ns/${namespace} but is sealed to an Age recipient this host does not hold; ` +
|
|
4335
|
-
`skipping. Adopt the store's key, re-key it from a host that holds one, or re-onboard from the ` +
|
|
4336
|
-
`origin seed files with --force.`,
|
|
4337
|
-
);
|
|
4338
|
-
continue;
|
|
4339
|
-
}
|
|
4340
|
-
|
|
4341
|
-
// Data keys come from the secret's origin seed contract, so an onboarded manifest
|
|
4342
|
-
// carries exactly the keys the workload's secretKeyRef already expects.
|
|
4343
|
-
const seedSources = Underpost.secret.sops.seedSources(name);
|
|
4344
|
-
const dataKeys = Object.keys(seedSources).length > 0 ? Object.keys(seedSources) : ['password'];
|
|
4345
|
-
const stringData = {};
|
|
4346
|
-
for (const key of dataKeys) {
|
|
4347
|
-
const seedPath = seedSources[key];
|
|
4348
|
-
if (seedPath && fs.existsSync(seedPath)) {
|
|
4349
|
-
stringData[key] = fs.readFileSync(seedPath, 'utf8').trim();
|
|
4350
|
-
logger.info(`${name}.${key} seeded from ${seedPath}`);
|
|
4351
|
-
} else if (overrides[key] !== undefined) {
|
|
4352
|
-
stringData[key] = overrides[key];
|
|
4353
|
-
logger.info(`${name}.${key} taken from --args`);
|
|
4354
|
-
} else {
|
|
4355
|
-
stringData[key] = generateSeedValue(key);
|
|
4356
|
-
// Replacing a stored manifest with a value nothing seeded means the credential the
|
|
4357
|
-
// running datastore still authenticates against is being thrown away.
|
|
4358
|
-
if (stored)
|
|
4359
|
-
logger.warn(
|
|
4360
|
-
`${name}.${key} generated while replacing the stored manifest — no seed file at ` +
|
|
4361
|
-
`${seedPath || '(unmapped)'} and no --args override. The running datastore keeps its old ` +
|
|
4362
|
-
`credential until this value is applied to it; pass --args "${key}=<value>" to keep the ` +
|
|
4363
|
-
`existing one.`,
|
|
4364
|
-
);
|
|
4365
|
-
else logger.info(`${name}.${key} generated`);
|
|
4366
|
-
}
|
|
4367
|
-
}
|
|
4368
|
-
|
|
4369
|
-
const stagePath = `${stageDir}/${name}.yaml`;
|
|
4370
|
-
fs.outputFileSync(
|
|
4371
|
-
stagePath,
|
|
4372
|
-
[
|
|
4373
|
-
'apiVersion: v1',
|
|
4374
|
-
'kind: Secret',
|
|
4375
|
-
'metadata:',
|
|
4376
|
-
` name: ${name}`,
|
|
4377
|
-
` namespace: ${namespace}`,
|
|
4378
|
-
' labels:',
|
|
4379
|
-
' app.kubernetes.io/managed-by: underpost',
|
|
4380
|
-
'type: Opaque',
|
|
4381
|
-
'stringData:',
|
|
4382
|
-
// Single-quoted YAML scalars with doubled internal quotes: values are generated or
|
|
4383
|
-
// operator-supplied and may contain characters YAML would otherwise interpret.
|
|
4384
|
-
...Object.entries(stringData).map(([key, value]) => ` ${key}: '${`${value}`.replace(/'/g, "''")}'`),
|
|
4385
|
-
'',
|
|
4386
|
-
].join('\n'),
|
|
4387
|
-
'utf8',
|
|
4388
|
-
);
|
|
4389
|
-
fs.chmodSync(stagePath, 0o600);
|
|
4390
|
-
// encrypt() stages, validates, moves into place, and shreds the plaintext source.
|
|
4391
|
-
Underpost.secret.sops.encrypt(stagePath, namespace, options);
|
|
4392
|
-
}
|
|
4393
|
-
} finally {
|
|
4394
|
-
// Defense in depth: encrypt() shreds each source, but a throw mid-loop must not leave a
|
|
4395
|
-
// plaintext manifest sitting in shared memory.
|
|
4396
|
-
fs.removeSync(stageDir);
|
|
4397
|
-
}
|
|
4398
|
-
|
|
4399
|
-
Underpost.secret.sops.list();
|
|
4400
|
-
|
|
4401
|
-
// 4. Validate every manifest in the namespace, then apply unless this is a dry run.
|
|
4402
|
-
Underpost.secret.sops.apply(namespace, { dryRun: true });
|
|
4403
|
-
if (options.dryRun) return logger.info('--dry-run: validated only, cluster left unchanged');
|
|
4404
|
-
Underpost.secret.sops.apply(namespace);
|
|
4405
|
-
},
|
|
4406
|
-
/**
|
|
4407
|
-
* @method sops-status
|
|
4408
|
-
* @description Reports the live state of the SOPS/Age secret system: host tooling, the Age
|
|
4409
|
-
* key and its recipient, the committed creation rules, every stored manifest with whether the
|
|
4410
|
-
* local key can open it and whether the cluster still matches, and which managed Secrets are
|
|
4411
|
-
* onboarded versus still seeding from their origin path.
|
|
4412
|
-
*
|
|
4413
|
-
* Read-only and safe to run anywhere. Decryption happens only for the drift check, only for
|
|
4414
|
-
* manifests the local key is a recipient of, and only into `kubectl diff` with its output
|
|
4415
|
-
* discarded — no secret value is ever printed or written to disk.
|
|
4416
|
-
*
|
|
4417
|
-
* Usage:
|
|
4418
|
-
* underpost run sops-status # every managed key, ns default
|
|
4419
|
-
* underpost run sops-status mongo # partial match: both mongo keys
|
|
4420
|
-
* underpost run sops-status --namespace prod # every managed key in ns prod
|
|
4421
|
-
* @param {string} path - Comma-separated managed Secret keys to report on; empty reports all.
|
|
4422
|
-
* Matched as case-insensitive substrings (`mongo` selects mongodb-secret and mongodb-keyfile).
|
|
4423
|
-
* Filters both the stored-manifest listing and the coverage table.
|
|
4424
|
-
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
4425
|
-
* @param {string} options.namespace - Namespace to inspect (DEFAULT_OPTION scheme, default 'default').
|
|
4426
|
-
* @memberof UnderpostRun
|
|
4427
|
-
*/
|
|
4428
|
-
'sops-status': (path = '', options = DEFAULT_OPTION) => {
|
|
4429
|
-
const sops = Underpost.secret.sops;
|
|
4430
|
-
// `--namespace` selects the namespace (DEFAULT_OPTION scheme); `path` narrows which managed
|
|
4431
|
-
// Secret keys to report on, so the two axes stay independent.
|
|
4432
|
-
const namespace = options.namespace || 'default';
|
|
4433
|
-
const manageSecretKeyFilter = path
|
|
4434
|
-
.split(',')
|
|
4435
|
-
.map((key) => key.trim().toLowerCase())
|
|
4436
|
-
.filter(Boolean);
|
|
4437
|
-
// Partial, case-insensitive substring match, so `mongo` reaches both `mongodb-secret` and
|
|
4438
|
-
// `mongodb-keyfile` without having to spell either out.
|
|
4439
|
-
const matchesKeyFilter = (name) =>
|
|
4440
|
-
manageSecretKeyFilter.length === 0 || manageSecretKeyFilter.some((key) => name.toLowerCase().includes(key));
|
|
4441
|
-
const mark = (ok) => (ok ? 'yes' : 'no');
|
|
4442
|
-
|
|
4443
|
-
// ── Tooling ────────────────────────────────────────────────────────────
|
|
4444
|
-
const version = (bin, flag) =>
|
|
4445
|
-
sops.hasBinary(bin)
|
|
4446
|
-
? shellExec(`${bin} ${flag} 2>/dev/null | head -1`, { stdout: true, silent: true, disableLog: true }).trim()
|
|
4447
|
-
: '(not installed)';
|
|
4448
|
-
logger.info(
|
|
4449
|
-
'[sops-status] Tooling\n' +
|
|
4450
|
-
` sops ${version('sops', '--version')}\n` +
|
|
4451
|
-
` age ${version('age', '--version')}\n` +
|
|
4452
|
-
` age-keygen ${sops.hasBinary('age-keygen') ? 'installed' : '(not installed)'}`,
|
|
4453
|
-
);
|
|
4454
|
-
|
|
4455
|
-
// ── Age key ────────────────────────────────────────────────────────────
|
|
4456
|
-
const keyFile = sops.keyFile();
|
|
4457
|
-
const keyExists = fs.existsSync(keyFile);
|
|
4458
|
-
// A key file may hold several identities — that is how a host joins a store it did not
|
|
4459
|
-
// create — so every check below works against the whole held set, not one recipient.
|
|
4460
|
-
const held = sops.localRecipients();
|
|
4461
|
-
const keyMode = keyExists ? (fs.statSync(keyFile).mode & 0o777).toString(8) : '';
|
|
4462
|
-
logger.info(
|
|
4463
|
-
'[sops-status] Age key\n' +
|
|
4464
|
-
` path ${keyFile}\n` +
|
|
4465
|
-
` present ${mark(keyExists)}${keyExists ? ` (mode ${keyMode}${keyMode === '600' || keyMode === '400' ? '' : ' — INSECURE, run chmod 600'})` : ''}\n` +
|
|
4466
|
-
` recipients ${held.join(', ') || (keyExists ? '(none — unreadable key file)' : '(none)')}` +
|
|
4467
|
-
(keyExists ? '' : `\n searched ${sops.keyFileCandidates().join(', ')}`),
|
|
4468
|
-
);
|
|
4469
|
-
|
|
4470
|
-
// ── Creation rules ─────────────────────────────────────────────────────
|
|
4471
|
-
const confPath = './engine-private/secrets/.sops.yaml';
|
|
4472
|
-
const ruleRecipients = sops.creationRecipients();
|
|
4473
|
-
logger.info(
|
|
4474
|
-
'[sops-status] Creation rules\n' +
|
|
4475
|
-
` config ${confPath} ${fs.existsSync(confPath) ? '' : '(missing — run: underpost secret sops --init)'}\n` +
|
|
4476
|
-
` recipients ${ruleRecipients.length > 0 ? ruleRecipients.join(', ') : '(none)'}\n` +
|
|
4477
|
-
` local key listed ${mark(held.some((recipient) => ruleRecipients.includes(recipient)))}`,
|
|
4478
|
-
);
|
|
4479
|
-
|
|
4480
|
-
// ── Stored manifests ───────────────────────────────────────────────────
|
|
4481
|
-
const manifests = sops.manifests(namespace).filter((manifest) => matchesKeyFilter(manifest.name));
|
|
4482
|
-
const onboarded = new Set();
|
|
4483
|
-
if (manifests.length === 0)
|
|
4484
|
-
logger.warn(
|
|
4485
|
-
`[sops-status] Store\n no encrypted manifests in ns/${namespace}` +
|
|
4486
|
-
(manageSecretKeyFilter.length > 0 ? ` matching ${manageSecretKeyFilter.join(', ')}` : ''),
|
|
4487
|
-
);
|
|
4488
|
-
else {
|
|
4489
|
-
const rows = manifests.map((manifest) => {
|
|
4490
|
-
onboarded.add(manifest.name);
|
|
4491
|
-
const recipients = sops.manifestRecipients(manifest.path);
|
|
4492
|
-
const decryptable = sops.decryptable(manifest.path, held);
|
|
4493
|
-
const live = shellExec(
|
|
4494
|
-
`kubectl get secret ${manifest.name} -n ${manifest.namespace} --ignore-not-found -o name 2>/dev/null || true`,
|
|
4495
|
-
{ stdout: true, silent: true, silentOnError: true, disableLog: true },
|
|
4496
|
-
).trim();
|
|
4497
|
-
// Drift is decided by kubectl's exit code; its stdout would contain the decrypted
|
|
4498
|
-
// values, so it is discarded rather than captured.
|
|
4499
|
-
let sync = 'n/a';
|
|
4500
|
-
if (live && decryptable) {
|
|
4501
|
-
const result = shellExec(
|
|
4502
|
-
`bash -c 'set -o pipefail; SOPS_AGE_KEY_FILE="${keyFile}" sops --decrypt "${manifest.path}" ` +
|
|
4503
|
-
`| kubectl diff -f - -n "${manifest.namespace}" >/dev/null 2>&1'`,
|
|
4504
|
-
{ silentOnError: true, disableLog: true, stdout: false },
|
|
4505
|
-
);
|
|
4506
|
-
sync = result.code === 0 ? 'in-sync' : result.code === 1 ? 'DRIFT' : 'error';
|
|
4507
|
-
} else if (!live) sync = 'not applied';
|
|
4508
|
-
else if (!decryptable) sync = 'no local key';
|
|
4509
|
-
return (
|
|
4510
|
-
` ${`${manifest.namespace}/${manifest.name}`.padEnd(34)} ` +
|
|
4511
|
-
`recipients=${String(recipients.length).padEnd(3)} ` +
|
|
4512
|
-
`decryptable=${mark(decryptable).padEnd(4)} ` +
|
|
4513
|
-
`live=${mark(!!live).padEnd(4)} ` +
|
|
4514
|
-
`${sync}`
|
|
4515
|
-
);
|
|
4516
|
-
});
|
|
4517
|
-
logger.info(`[sops-status] Store — ns/${namespace} (${manifests.length} manifest(s))\n` + rows.join('\n'));
|
|
4518
|
-
}
|
|
4519
|
-
|
|
4520
|
-
// ── Coverage ───────────────────────────────────────────────────────────
|
|
4521
|
-
const coverage = sops
|
|
4522
|
-
.managedSecrets()
|
|
4523
|
-
.filter(matchesKeyFilter)
|
|
4524
|
-
.map((name) => {
|
|
4525
|
-
const seeds = Object.values(sops.seedSources(name));
|
|
4526
|
-
const seedPresent = seeds.length > 0 && seeds.every((seed) => fs.existsSync(seed));
|
|
4527
|
-
const source = onboarded.has(name)
|
|
4528
|
-
? 'sops'
|
|
4529
|
-
: seedPresent
|
|
4530
|
-
? 'origin seed'
|
|
4531
|
-
: seeds.length
|
|
4532
|
-
? 'MISSING'
|
|
4533
|
-
: 'unmapped';
|
|
4534
|
-
return ` ${name.padEnd(24)} ${source.padEnd(12)} ${seeds.length ? `seed=${mark(seedPresent)}` : ''}`;
|
|
4535
|
-
});
|
|
4536
|
-
if (coverage.length === 0)
|
|
4537
|
-
logger.warn(
|
|
4538
|
-
`[sops-status] Coverage\n no managed Secret matches ${manageSecretKeyFilter.join(', ')}\n` +
|
|
4539
|
-
` known keys: ${sops.managedSecrets().join(', ')}`,
|
|
4540
|
-
);
|
|
4541
|
-
else
|
|
4542
|
-
logger.info('[sops-status] Coverage (which source each managed Secret deploys from)\n' + coverage.join('\n'));
|
|
4543
|
-
},
|
|
4544
|
-
/**
|
|
4545
|
-
* @method secret
|
|
4546
|
-
* @description Creates an Underpost secret named 'underpost' from a file, defaulting to `/home/dd/engine/engine-private/conf/dd-cron/.env.production` if no path is provided.
|
|
4547
|
-
* @param {string} path - The input value, identifier, or path for the operation (used as the optional path to the secret file).
|
|
4548
|
-
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
4549
|
-
* @memberof UnderpostRun
|
|
4550
|
-
*/
|
|
4551
|
-
secret: (path, options = DEFAULT_OPTION) => {
|
|
4552
|
-
const cronDeployId = cronDeployIdResolve() || 'dd-cron';
|
|
4553
|
-
Underpost.secret.underpost.createFromEnvFile(
|
|
4554
|
-
`/home/dd/engine/engine-private/conf/${cronDeployId}/.env.${options.dev ? 'development' : 'production'}`,
|
|
4555
|
-
);
|
|
4556
|
-
},
|
|
4557
|
-
/**
|
|
4558
|
-
* @method underpost-config
|
|
4559
|
-
* @description Calls `Underpost.deploy.configMap` to create a Kubernetes ConfigMap, defaulting to the 'production' environment.
|
|
4560
|
-
* @param {string} path - The input value, identifier, or path for the operation (used as the optional configuration name/environment).
|
|
4561
|
-
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow
|
|
4562
|
-
* @memberof UnderpostRun
|
|
4563
|
-
*/
|
|
4564
|
-
'underpost-config': (path = '', options = DEFAULT_OPTION) => {
|
|
4565
|
-
Underpost.deploy.configMap(path ? path : 'production', options.namespace);
|
|
4566
|
-
},
|
|
4567
4240
|
/**
|
|
4568
4241
|
* @method gpu-env
|
|
4569
4242
|
* @description Sets up a dedicated GPU development environment cluster, resetting and then setting up the cluster with `--dedicated-gpu` and monitoring the pods.
|
|
@@ -4659,7 +4332,7 @@ UNDERPOST_SOPS_ENV_EOF`,
|
|
|
4659
4332
|
? 'Directory'
|
|
4660
4333
|
: 'File';
|
|
4661
4334
|
|
|
4662
|
-
const envs = Underpost.
|
|
4335
|
+
const envs = Underpost.host.store.list();
|
|
4663
4336
|
|
|
4664
4337
|
const cmd = `kubectl apply -f - <<'EOF'
|
|
4665
4338
|
apiVersion: ${apiVersion}
|
|
@@ -4736,7 +4409,7 @@ EOF`;
|
|
|
4736
4409
|
* @memberof UnderpostRun
|
|
4737
4410
|
*/
|
|
4738
4411
|
'push-bundle': (path = '', options = DEFAULT_OPTION) => {
|
|
4739
|
-
const baseCommand = '
|
|
4412
|
+
const baseCommand = cli('underpost', { local: true });
|
|
4740
4413
|
const env = options.dev ? 'development' : 'production';
|
|
4741
4414
|
const deployId = options.deployId || 'dd-default';
|
|
4742
4415
|
const pathParts = (path || '').split('.');
|
|
@@ -4767,7 +4440,7 @@ EOF`;
|
|
|
4767
4440
|
const storageFilePath = `engine-private/conf/${deployId}/storage.bundle.json`;
|
|
4768
4441
|
|
|
4769
4442
|
shellExec(`${baseCommand} env ${deployId} ${env}`);
|
|
4770
|
-
shellExec(`${baseCommand} client ${deployId} --build-zip${splitFlag ? ` ${splitFlag}` : ''}`);
|
|
4443
|
+
shellExec(`${baseCommand} client ${deployId} --env ${env} --build-zip${splitFlag ? ` ${splitFlag}` : ''}`);
|
|
4771
4444
|
|
|
4772
4445
|
const pushBundleFiles = (host, routePath) => {
|
|
4773
4446
|
const buildId = `${host}-${routePath.replaceAll('/', '')}`;
|
|
@@ -4824,7 +4497,7 @@ EOF`;
|
|
|
4824
4497
|
* @memberof UnderpostRun
|
|
4825
4498
|
*/
|
|
4826
4499
|
'pull-bundle': (path = '', options = DEFAULT_OPTION) => {
|
|
4827
|
-
const baseCommand = '
|
|
4500
|
+
const baseCommand = cli('underpost', { local: true });
|
|
4828
4501
|
const env = options.dev ? 'development' : 'production';
|
|
4829
4502
|
const deployId = options.deployId || 'dd-default';
|
|
4830
4503
|
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
@@ -4906,17 +4579,52 @@ EOF`;
|
|
|
4906
4579
|
}
|
|
4907
4580
|
},
|
|
4908
4581
|
|
|
4582
|
+
/**
|
|
4583
|
+
* @method kubeadm-wireguard
|
|
4584
|
+
* @description
|
|
4585
|
+
* Configures Calico to keep using the Kubernetes NodeInternalIP after WireGuard
|
|
4586
|
+
* is added to the node. This prevents the WireGuard interface (for example
|
|
4587
|
+
* 10.0.0.2) from being selected as the Calico/BGP node address.
|
|
4588
|
+
*
|
|
4589
|
+
* @param {string} path - Unused.
|
|
4590
|
+
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options.
|
|
4591
|
+
* @memberof UnderpostRun
|
|
4592
|
+
*/
|
|
4593
|
+
'kubeadm-wireguard': (path, options = DEFAULT_OPTION) => {
|
|
4594
|
+
shellExec(`kubectl patch installation.operator.tigera.io default --type='json' \
|
|
4595
|
+
-p='[
|
|
4596
|
+
{"op":"replace","path":"/spec/calicoNetwork/nodeAddressAutodetectionV4","value":{"kubernetes":"NodeInternalIP"}}
|
|
4597
|
+
]'`);
|
|
4598
|
+
|
|
4599
|
+
shellExec(`kubectl rollout restart daemonset/calico-node -n calico-system`);
|
|
4600
|
+
},
|
|
4601
|
+
|
|
4909
4602
|
/**
|
|
4910
4603
|
* @method build-cluster-deployment-manifests
|
|
4911
|
-
* @description Builds deployment manifests for both production and development environments using `
|
|
4604
|
+
* @description Builds deployment manifests for both production and development environments using `underpost deploy --build-manifest`, syncing them, and setting replicas to 1 for the `dd` deployment. Runs under `HERMETIC_BUILD`, so it needs no reachable cluster.
|
|
4912
4605
|
* @param {string} path - Unused.
|
|
4913
4606
|
* @param {UnderpostRunDefaultOptions} options - The default underpost runner options for customizing workflow.
|
|
4914
4607
|
* @memberof UnderpostRun
|
|
4915
4608
|
*/
|
|
4916
|
-
'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) =>
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4609
|
+
'build-cluster-deployment-manifests': (path = '', options = DEFAULT_OPTION) =>
|
|
4610
|
+
// Manifest generation writes files; it does not touch a cluster. Declaring that once
|
|
4611
|
+
// here is what lets the whole tree below run on a build box with no apiserver — the
|
|
4612
|
+
// profile rides into both child processes through the environment.
|
|
4613
|
+
withExecutionProfile('HERMETIC_BUILD', async () => {
|
|
4614
|
+
// Each deploy's package manifest is generated from the engine's before the cluster
|
|
4615
|
+
// manifests that ship it: a deploy id declares only its identity and whatever its
|
|
4616
|
+
// catalog adds, so a dependency the engine gained reaches every deploy from here.
|
|
4617
|
+
await syncDeployPackages();
|
|
4618
|
+
// `local` is load-bearing: this builds the manifests of the checkout it was
|
|
4619
|
+
// launched from, and a globally installed underpost is a different package that
|
|
4620
|
+
// reads different paths out of the same cwd.
|
|
4621
|
+
const underpost = cli('underpost', { local: true });
|
|
4622
|
+
const flags = '--build-manifest --sync --info-router --replicas 1';
|
|
4623
|
+
shellExec(`${underpost} deploy ${flags} dd development`);
|
|
4624
|
+
shellExec(`${underpost} deploy ${flags} dd production --cert`);
|
|
4625
|
+
// What the build just produced is what the project repository carries.
|
|
4626
|
+
if (deployRoutesExists()) Underpost.deploy.mirrorDefaultManifests();
|
|
4627
|
+
}),
|
|
4920
4628
|
|
|
4921
4629
|
/**
|
|
4922
4630
|
* @method monitor-ui
|
|
@@ -5056,7 +4764,7 @@ EOF`;
|
|
|
5056
4764
|
const result = await Underpost.run.CALL(runner, path, options);
|
|
5057
4765
|
return result;
|
|
5058
4766
|
} catch (error) {
|
|
5059
|
-
console.
|
|
4767
|
+
console.error(error);
|
|
5060
4768
|
logger.error(error);
|
|
5061
4769
|
process.exit(1);
|
|
5062
4770
|
}
|