@underpostnet/cyberia 3.3.0 → 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/.github/workflows/coverall.cyberia.ci.yml +34 -14
- package/.github/workflows/cyberia-client.cd.yml +5 -0
- package/.github/workflows/cyberia-server.cd.yml +5 -0
- 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 +3 -0
- package/.github/workflows/engine-cyberia.ci.yml +4 -5
- package/.github/workflows/ghpkg.ci.yml +35 -20
- package/.github/workflows/hardhat.ci.yml +12 -11
- package/.github/workflows/npmpkg.ci.yml +17 -15
- package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
- package/.github/workflows/release.cd.yml +1 -0
- package/.prettierignore +1 -0
- package/AGENTS.md +47 -0
- package/CHANGELOG.md +565 -2510
- package/CLI-HELP.md +306 -147
- package/Dockerfile +25 -9
- package/Dockerfile.dev +25 -9
- package/Dockerfile.test +4 -4
- package/README.md +21 -1
- package/bin/build.js +66 -41
- package/bin/build.template.js +8 -3
- package/bin/cyberia.js +308 -91
- package/bin/deploy.js +18 -104
- package/bin/index.js +308 -91
- package/conf.js +91 -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 +6 -26
- package/deploy/dd-cyberia/init.sh +35 -43
- package/deploy/dd-cyberia/package.sh +17 -0
- package/deploy/dd-cyberia/state.sh +14 -0
- package/deploy/dd-cyberia/sync-deploy.sh +92 -170
- 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 +12 -39
- package/deploy/release/deploy.sh +9 -39
- package/deployment.yaml +7 -2
- package/docker-compose.yml +7 -7
- 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 +101 -40
- package/hardhat/package.json +2 -2
- package/manifests/alertmanager/deployment.yaml +81 -0
- package/manifests/blackbox-exporter/deployment.yaml +75 -0
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +32 -7
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +32 -7
- 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/event-monitor.sh +56 -0
- package/scripts/k3s-node-setup.sh +27 -14
- package/scripts/kubeadm-node-setup.sh +31 -11
- package/scripts/maas-setup.sh +8 -8
- 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-world.js +8 -0
- package/src/api/cyberia-instance/cyberia-instance-map.service.js +1 -1
- package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -1
- package/src/api/cyberia-instance/cyberia-instance.router.js +2 -2
- package/src/api/cyberia-instance/cyberia-instance.service.js +2 -2
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
- 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 +0 -26
- 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.router.js +2 -2
- package/src/api/object-layer/object-layer.service.js +3 -3
- 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 -35
- package/src/cli/client.js +306 -0
- package/src/cli/cloud-init.js +3 -3
- package/src/cli/cluster.js +184 -82
- package/src/cli/db.js +120 -68
- package/src/cli/deploy.js +153 -71
- 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 -6
- package/src/cli/index.js +322 -142
- 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 +16 -12
- package/src/cli/repository.js +558 -363
- package/src/cli/run.js +331 -650
- package/src/cli/secrets.js +2158 -964
- package/src/cli/ssh.js +478 -279
- package/src/cli/state.js +394 -0
- package/src/cli/static.js +2 -2
- package/src/cli/system.js +1 -1
- package/src/cli/test.js +199 -125
- package/src/cli/vultr.js +61 -21
- package/src/cli/wireguard.js +1111 -306
- 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-portal/AppShellCyberiaPortal.js +2 -1
- package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
- 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/CYBERIA-CLI.md +32 -5
- 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/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 +38 -30
- 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 +7 -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 +79 -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 +53 -10
- package/src/projects/cyberia/gemini-client.js +1 -1
- package/src/projects/cyberia/generate-saga.js +1 -1
- package/src/projects/cyberia/hot-reload-trigger.js +1 -1
- package/src/projects/cyberia/instance-data.js +1 -1
- 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 +1 -1
- package/src/projects/cyberia/semantic-layer-generator.js +1 -1
- package/src/projects/underpost/catalog-underpost.js +9 -1
- 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/docker-compose.yml +7 -7
- 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} +144 -11
- package/src/server/{forward-proxy.js → network/forward-proxy.js} +10 -18
- package/src/server/{middlewares.js → network/middlewares.js} +3 -3
- 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/{underpost-compression.js → network/underpost-compression.js} +1 -1
- package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +7 -7
- package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +2 -2
- package/src/server/{cri.js → ops/cri.js} +2 -2
- package/src/server/{cron.js → ops/cron.js} +349 -74
- 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/{systemd.js → ops/systemd.js} +55 -1
- package/src/server/{conf.js → runtime/conf.js} +513 -422
- 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 -12
- 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/{selinux.js → security/selinux.js} +1 -1
- 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} +1 -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/{selinux.test.js → integration/infra/1-security/selinux.test.js} +2 -4
- 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/{wireguard-edge.test.js → integration/infra/2-network/wireguard-edge.test.js} +609 -55
- 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} +6 -6
- package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +4 -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/deploy/lib/logging.sh +0 -96
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
- package/manifests/mongodb/pv-pvc.yaml +0 -59
- package/scripts/coverall-test-cyberia.sh +0 -24
- package/scripts/coverall-test.sh +0 -24
- 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/environment.js +0 -98
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -Eeuo pipefail
|
|
3
|
+
|
|
4
|
+
# Gracefully quiesce this kubeadm node and power it off for physical relocation.
|
|
5
|
+
#
|
|
6
|
+
# Normal use:
|
|
7
|
+
# sudo ./scripts/shutdown-machine.sh
|
|
8
|
+
#
|
|
9
|
+
# The internal restore mode is invoked by systemd after the next boot. It may
|
|
10
|
+
# also be run manually if automatic workload restoration needs to be retried:
|
|
11
|
+
# sudo /usr/local/sbin/engine-shutdown-machine --restore-statefulsets
|
|
12
|
+
|
|
13
|
+
readonly PROGRAM="${0##*/}"
|
|
14
|
+
readonly SCRIPT_PATH="$(readlink -f -- "${BASH_SOURCE[0]}")"
|
|
15
|
+
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
16
|
+
readonly ENGINE_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd -P)"
|
|
17
|
+
readonly STATE_DIR="/var/lib/engine/shutdown-machine"
|
|
18
|
+
readonly STATE_FILE="${STATE_DIR}/statefulsets.tsv"
|
|
19
|
+
readonly CONTROLLER_STATE_FILE="${STATE_DIR}/controllers.tsv"
|
|
20
|
+
readonly NODE_FILE="${STATE_DIR}/node-name"
|
|
21
|
+
readonly KUBECONFIG_FILE="${STATE_DIR}/kubeconfig-path"
|
|
22
|
+
readonly ENGINE_ROOT_FILE="${STATE_DIR}/engine-root"
|
|
23
|
+
readonly NODE_BIN_FILE="${STATE_DIR}/node-bin"
|
|
24
|
+
readonly EVENT_STATE_FILE="${STATE_DIR}/events.json"
|
|
25
|
+
readonly MONGODB_QUIESCED_MARKER="${STATE_DIR}/mongodb-quiesced"
|
|
26
|
+
readonly MONGODB_SECRETS_MARKER="${STATE_DIR}/mongodb-secrets-restored"
|
|
27
|
+
readonly MONGODB_REPLICA_MARKER="${STATE_DIR}/mongodb-replica-restored"
|
|
28
|
+
readonly UNCORDON_MARKER="${STATE_DIR}/uncordon-on-restore"
|
|
29
|
+
readonly RESTORE_BIN="/usr/local/sbin/engine-shutdown-machine"
|
|
30
|
+
readonly RESTORE_UNIT="engine-kubernetes-restore.service"
|
|
31
|
+
readonly RESTORE_UNIT_FILE="/etc/systemd/system/${RESTORE_UNIT}"
|
|
32
|
+
readonly ETCD_BACKUP_DIR="/var/lib/etcd/pre-shutdown-snapshots"
|
|
33
|
+
|
|
34
|
+
NODE_NAME=""
|
|
35
|
+
KUBE_NODE_NAME=""
|
|
36
|
+
LAN_IP=""
|
|
37
|
+
ROLE=""
|
|
38
|
+
NODE_BIN=""
|
|
39
|
+
CORDONED_BY_SCRIPT=0
|
|
40
|
+
WORKLOAD_STATE_SAVED=0
|
|
41
|
+
SERVICES_STOPPED=0
|
|
42
|
+
ROLLBACK_RUNNING=0
|
|
43
|
+
declare -a ACTIVE_RUNTIMES=()
|
|
44
|
+
|
|
45
|
+
log_info() {
|
|
46
|
+
printf '%s [INFO] [%s] %s\n' "$(date --iso-8601=seconds)" "${PROGRAM}" "$*"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
log_warn() {
|
|
50
|
+
printf '%s [WARN] [%s] %s\n' "$(date --iso-8601=seconds)" "${PROGRAM}" "$*" >&2
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
log_error() {
|
|
54
|
+
printf '%s [ERROR] [%s] %s\n' "$(date --iso-8601=seconds)" "${PROGRAM}" "$*" >&2
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
die() {
|
|
58
|
+
log_error "$*"
|
|
59
|
+
return 1
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
require_root() {
|
|
63
|
+
if (( EUID != 0 )); then
|
|
64
|
+
die "This script must be run as root (for example: sudo ${SCRIPT_PATH})."
|
|
65
|
+
fi
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
require_command() {
|
|
69
|
+
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# NVM installs Node.js below a user's home directory and exposes it through
|
|
73
|
+
# interactive-shell PATH setup. systemd does not load that setup, so persist and
|
|
74
|
+
# invoke the canonical executable instead of relying on a bare `node` command.
|
|
75
|
+
# The PATH fallback keeps recovery compatible with state written by an older
|
|
76
|
+
# version of this script when an administrator runs it with an explicit PATH.
|
|
77
|
+
resolve_node_binary() {
|
|
78
|
+
local candidate=""
|
|
79
|
+
|
|
80
|
+
if [[ -s "${NODE_BIN_FILE}" ]]; then
|
|
81
|
+
IFS= read -r candidate < "${NODE_BIN_FILE}"
|
|
82
|
+
else
|
|
83
|
+
candidate="$(command -v node 2>/dev/null || true)"
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
[[ -n "${candidate}" ]] || die "Node.js executable was not found; NVM users must preserve its bin directory in PATH for this recovery."
|
|
87
|
+
[[ "${candidate}" == /* ]] || die "Node.js executable path is not absolute: ${candidate}"
|
|
88
|
+
[[ "${candidate}" != *$'\n'* ]] || die "Node.js executable path must not contain a newline."
|
|
89
|
+
[[ -x "${candidate}" ]] || die "Saved Node.js executable is missing or not executable: ${candidate}"
|
|
90
|
+
|
|
91
|
+
NODE_BIN="$(readlink -f -- "${candidate}")"
|
|
92
|
+
[[ -x "${NODE_BIN}" ]] || die "Canonical Node.js executable is missing or not executable: ${NODE_BIN}"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Select credentials that work both now and from the post-boot systemd unit.
|
|
96
|
+
# API reachability is checked separately because the control-plane API may still
|
|
97
|
+
# be starting when the restore unit first runs.
|
|
98
|
+
resolve_kubeconfig() {
|
|
99
|
+
local candidate=""
|
|
100
|
+
|
|
101
|
+
if [[ -s "${KUBECONFIG_FILE}" ]]; then
|
|
102
|
+
IFS= read -r candidate < "${KUBECONFIG_FILE}"
|
|
103
|
+
if [[ -n "${candidate}" ]]; then
|
|
104
|
+
export KUBECONFIG="${candidate}"
|
|
105
|
+
return 0
|
|
106
|
+
fi
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
if kubectl version --request-timeout=10s >/dev/null 2>&1; then
|
|
110
|
+
return 0
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
for candidate in /etc/kubernetes/admin.conf /root/.kube/config; do
|
|
114
|
+
if [[ -r "${candidate}" ]]; then
|
|
115
|
+
export KUBECONFIG="${candidate}"
|
|
116
|
+
return 0
|
|
117
|
+
fi
|
|
118
|
+
done
|
|
119
|
+
|
|
120
|
+
die "No readable Kubernetes kubeconfig was found."
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
check_prerequisites() {
|
|
124
|
+
require_root
|
|
125
|
+
require_command kubectl
|
|
126
|
+
require_command systemctl
|
|
127
|
+
require_command sync
|
|
128
|
+
require_command shutdown
|
|
129
|
+
require_command install
|
|
130
|
+
require_command readlink
|
|
131
|
+
resolve_node_binary
|
|
132
|
+
resolve_kubeconfig
|
|
133
|
+
kubectl version --request-timeout=10s >/dev/null 2>&1 \
|
|
134
|
+
|| die "kubectl cannot reach the Kubernetes API with the available kubeconfig."
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
resolve_node_environment() {
|
|
138
|
+
local metadata_file raw_ip
|
|
139
|
+
|
|
140
|
+
NODE_NAME="$(hostname)"
|
|
141
|
+
metadata_file="${ENGINE_ROOT}/engine-private/deploy/nodes/${NODE_NAME}.json"
|
|
142
|
+
[[ -r "${metadata_file}" ]] || die "Node metadata file is missing or unreadable: ${metadata_file}"
|
|
143
|
+
|
|
144
|
+
# This is the repository's authoritative LAN-address resolver.
|
|
145
|
+
if ! raw_ip="$(cd "${ENGINE_ROOT}" && "${NODE_BIN}" bin ip --dhcp)"; then
|
|
146
|
+
die "Failed to resolve the LAN address with 'node bin ip --dhcp'."
|
|
147
|
+
fi
|
|
148
|
+
LAN_IP="$(printf '%s\n' "${raw_ip}" | tr -d '\r' | awk 'NF { value=$0 } END { print value }')"
|
|
149
|
+
if [[ ! "${LAN_IP}" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
|
150
|
+
die "'node bin ip --dhcp' did not return an IPv4 address (received: ${LAN_IP:-<empty>})."
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
ROLE="$("${NODE_BIN}" -e '
|
|
154
|
+
const fs = require("fs");
|
|
155
|
+
const file = process.argv[1];
|
|
156
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8")).role;
|
|
157
|
+
if (value !== "control" && value !== "worker") process.exit(2);
|
|
158
|
+
process.stdout.write(value);
|
|
159
|
+
' "${metadata_file}")" || die "Node metadata role must be either 'control' or 'worker': ${metadata_file}"
|
|
160
|
+
|
|
161
|
+
KUBE_NODE_NAME="${NODE_NAME}"
|
|
162
|
+
if ! kubectl get node "${KUBE_NODE_NAME}" --request-timeout=10s >/dev/null 2>&1; then
|
|
163
|
+
if ! KUBE_NODE_NAME="$(kubectl get nodes \
|
|
164
|
+
-o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{range .status.addresses[?(@.type=="InternalIP")]}{.address}{end}{"\n"}{end}' \
|
|
165
|
+
| awk -v ip="${LAN_IP}" '$2 == ip { print $1; exit }')"; then
|
|
166
|
+
die "Failed to list Kubernetes nodes while matching LAN IP ${LAN_IP}."
|
|
167
|
+
fi
|
|
168
|
+
[[ -n "${KUBE_NODE_NAME}" ]] || die "No Kubernetes node matches hostname '${NODE_NAME}' or LAN IP '${LAN_IP}'."
|
|
169
|
+
log_warn "Hostname '${NODE_NAME}' differs from Kubernetes node name '${KUBE_NODE_NAME}'; using the latter."
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
log_info "Resolved node=${KUBE_NODE_NAME}, hostname=${NODE_NAME}, LAN_IP=${LAN_IP}, role=${ROLE}."
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
is_system_namespace() {
|
|
176
|
+
case "$1" in
|
|
177
|
+
kube-system|kube-public|kube-node-lease) return 0 ;;
|
|
178
|
+
*) return 1 ;;
|
|
179
|
+
esac
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Save desired replica counts before changing any StatefulSet. Scaling to zero
|
|
183
|
+
# persists in the API, so this state plus the one-shot unit is what makes the
|
|
184
|
+
# workloads return automatically after the relocated host boots.
|
|
185
|
+
prepare_post_boot_restore() {
|
|
186
|
+
local namespace name replicas tmp_file kubeconfig_value statefulsets
|
|
187
|
+
|
|
188
|
+
install -d -m 0700 "${STATE_DIR}"
|
|
189
|
+
if [[ -e "${STATE_FILE}" ]]; then
|
|
190
|
+
die "Unfinished shutdown state exists at ${STATE_FILE}; run '${RESTORE_BIN} --restore-statefulsets' first."
|
|
191
|
+
fi
|
|
192
|
+
if [[ -e "${EVENT_STATE_FILE}" ]]; then
|
|
193
|
+
die "Unfinished event suspension exists at ${EVENT_STATE_FILE}; run 'node bin event --resume-events ${EVENT_STATE_FILE}' first."
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
tmp_file="$(mktemp "${STATE_DIR}/.statefulsets.XXXXXX")"
|
|
197
|
+
if ! statefulsets="$(kubectl get statefulsets --all-namespaces \
|
|
198
|
+
-o go-template='{{range .items}}{{.metadata.namespace}}{{"\t"}}{{.metadata.name}}{{"\t"}}{{if .spec.replicas}}{{.spec.replicas}}{{else}}0{{end}}{{"\n"}}{{end}}')"; then
|
|
199
|
+
rm -f -- "${tmp_file}"
|
|
200
|
+
die "Failed to list StatefulSets; no Kubernetes resources were changed."
|
|
201
|
+
fi
|
|
202
|
+
while IFS=$'\t' read -r namespace name replicas; do
|
|
203
|
+
[[ -n "${namespace}" && -n "${name}" ]] || continue
|
|
204
|
+
is_system_namespace "${namespace}" && continue
|
|
205
|
+
[[ "${replicas}" =~ ^[0-9]+$ ]] || replicas=0
|
|
206
|
+
printf '%s\t%s\t%s\n' "${namespace}" "${name}" "${replicas}" >> "${tmp_file}"
|
|
207
|
+
done <<< "${statefulsets}"
|
|
208
|
+
chmod 0600 "${tmp_file}"
|
|
209
|
+
mv -f -- "${tmp_file}" "${STATE_FILE}"
|
|
210
|
+
: > "${CONTROLLER_STATE_FILE}"
|
|
211
|
+
chmod 0600 "${CONTROLLER_STATE_FILE}"
|
|
212
|
+
|
|
213
|
+
printf '%s\n' "${KUBE_NODE_NAME}" > "${NODE_FILE}"
|
|
214
|
+
chmod 0600 "${NODE_FILE}"
|
|
215
|
+
printf '%s\n' "${ENGINE_ROOT}" > "${ENGINE_ROOT_FILE}"
|
|
216
|
+
chmod 0600 "${ENGINE_ROOT_FILE}"
|
|
217
|
+
printf '%s\n' "${NODE_BIN}" > "${NODE_BIN_FILE}"
|
|
218
|
+
chmod 0600 "${NODE_BIN_FILE}"
|
|
219
|
+
|
|
220
|
+
kubeconfig_value="${KUBECONFIG:-/root/.kube/config}"
|
|
221
|
+
[[ "${kubeconfig_value}" != *$'\n'* ]] || die "KUBECONFIG must not contain a newline."
|
|
222
|
+
printf '%s\n' "${kubeconfig_value}" > "${KUBECONFIG_FILE}"
|
|
223
|
+
chmod 0600 "${KUBECONFIG_FILE}"
|
|
224
|
+
WORKLOAD_STATE_SAVED=1
|
|
225
|
+
|
|
226
|
+
# Install a stable copy because systemd must be able to restore even if the
|
|
227
|
+
# repository is not mounted or has moved by the time the unit runs.
|
|
228
|
+
install -m 0755 "${SCRIPT_PATH}" "${RESTORE_BIN}"
|
|
229
|
+
tee "${RESTORE_UNIT_FILE}" >/dev/null <<EOF
|
|
230
|
+
[Unit]
|
|
231
|
+
Description=Restore Kubernetes workloads after a graceful physical-node shutdown
|
|
232
|
+
Wants=network-online.target kubelet.service
|
|
233
|
+
After=network-online.target kubelet.service
|
|
234
|
+
ConditionPathExists=${STATE_FILE}
|
|
235
|
+
StartLimitIntervalSec=0
|
|
236
|
+
|
|
237
|
+
[Service]
|
|
238
|
+
Type=oneshot
|
|
239
|
+
ExecStart=${RESTORE_BIN} --restore-statefulsets
|
|
240
|
+
Restart=on-failure
|
|
241
|
+
RestartSec=15s
|
|
242
|
+
TimeoutStartSec=2h
|
|
243
|
+
|
|
244
|
+
[Install]
|
|
245
|
+
WantedBy=multi-user.target
|
|
246
|
+
EOF
|
|
247
|
+
chmod 0644 "${RESTORE_UNIT_FILE}"
|
|
248
|
+
systemctl daemon-reload
|
|
249
|
+
systemctl enable "${RESTORE_UNIT}" >/dev/null
|
|
250
|
+
log_info "Saved workload replica state and enabled post-boot restoration."
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Remove generated event probes and alert rules before workloads intentionally
|
|
254
|
+
# disappear. event.js atomically records the exact deployed set first, so any
|
|
255
|
+
# failure, rollback, or reboot can resynchronize that set without guessing.
|
|
256
|
+
suspend_events() {
|
|
257
|
+
log_info "Suspending operational events to prevent planned-shutdown alerts."
|
|
258
|
+
(
|
|
259
|
+
cd "${ENGINE_ROOT}"
|
|
260
|
+
"${NODE_BIN}" bin event --suspend-events "${EVENT_STATE_FILE}" --namespace default
|
|
261
|
+
)
|
|
262
|
+
[[ -s "${EVENT_STATE_FILE}" ]] || die "Event suspension did not create recovery state: ${EVENT_STATE_FILE}"
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
resume_events() {
|
|
266
|
+
local event_engine_root
|
|
267
|
+
[[ -e "${EVENT_STATE_FILE}" ]] || return 0
|
|
268
|
+
[[ -s "${ENGINE_ROOT_FILE}" ]] || die "Engine-root recovery state is missing: ${ENGINE_ROOT_FILE}"
|
|
269
|
+
IFS= read -r event_engine_root < "${ENGINE_ROOT_FILE}"
|
|
270
|
+
[[ -d "${event_engine_root}" ]] || die "Saved engine root is unavailable: ${event_engine_root}"
|
|
271
|
+
|
|
272
|
+
log_info "Resynchronizing operational events after workload recovery."
|
|
273
|
+
(
|
|
274
|
+
cd "${event_engine_root}"
|
|
275
|
+
"${NODE_BIN}" bin event --resume-events "${EVENT_STATE_FILE}"
|
|
276
|
+
)
|
|
277
|
+
[[ ! -e "${EVENT_STATE_FILE}" ]] || die "Event recovery state remains after resynchronization: ${EVENT_STATE_FILE}"
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
cordon_node() {
|
|
281
|
+
local was_unschedulable
|
|
282
|
+
if ! was_unschedulable="$(kubectl get node "${KUBE_NODE_NAME}" -o jsonpath='{.spec.unschedulable}')"; then
|
|
283
|
+
die "Failed to read schedulability for node ${KUBE_NODE_NAME}."
|
|
284
|
+
fi
|
|
285
|
+
if [[ "${was_unschedulable}" == "true" ]]; then
|
|
286
|
+
log_warn "Node ${KUBE_NODE_NAME} was already cordoned; it will remain cordoned after boot."
|
|
287
|
+
return 0
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
kubectl cordon "${KUBE_NODE_NAME}"
|
|
291
|
+
: > "${UNCORDON_MARKER}"
|
|
292
|
+
chmod 0600 "${UNCORDON_MARKER}"
|
|
293
|
+
CORDONED_BY_SCRIPT=1
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
snapshot_etcd() {
|
|
297
|
+
local etcd_pod timestamp snapshot_path snapshot_size
|
|
298
|
+
|
|
299
|
+
[[ "${ROLE}" == "control" ]] || return 0
|
|
300
|
+
install -d -m 0700 "${ETCD_BACKUP_DIR}"
|
|
301
|
+
|
|
302
|
+
if ! etcd_pod="$(kubectl -n kube-system get pods -l component=etcd \
|
|
303
|
+
--field-selector "spec.nodeName=${KUBE_NODE_NAME}" \
|
|
304
|
+
-o jsonpath='{.items[0].metadata.name}')"; then
|
|
305
|
+
die "Failed to query the local control-plane etcd static pod."
|
|
306
|
+
fi
|
|
307
|
+
[[ -n "${etcd_pod}" ]] || die "Could not find the local control-plane etcd static pod."
|
|
308
|
+
|
|
309
|
+
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
310
|
+
snapshot_path="${ETCD_BACKUP_DIR}/etcd-${KUBE_NODE_NAME}-${timestamp}.db"
|
|
311
|
+
log_info "Checking etcd health before snapshot."
|
|
312
|
+
kubectl -n kube-system exec "${etcd_pod}" -c etcd -- \
|
|
313
|
+
etcdctl --endpoints=https://127.0.0.1:2379 \
|
|
314
|
+
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
|
|
315
|
+
--cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
|
|
316
|
+
--key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
|
|
317
|
+
endpoint health
|
|
318
|
+
|
|
319
|
+
log_info "Saving etcd snapshot to ${snapshot_path}."
|
|
320
|
+
kubectl -n kube-system exec "${etcd_pod}" -c etcd -- \
|
|
321
|
+
etcdctl --endpoints=https://127.0.0.1:2379 \
|
|
322
|
+
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
|
|
323
|
+
--cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
|
|
324
|
+
--key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
|
|
325
|
+
snapshot save "${snapshot_path}"
|
|
326
|
+
|
|
327
|
+
[[ -s "${snapshot_path}" ]] || die "etcd reported success, but snapshot is absent or empty: ${snapshot_path}"
|
|
328
|
+
# etcdutl performs the snapshot integrity/hash check. Older etcd images may
|
|
329
|
+
# only expose the deprecated etcdctl equivalent, so retain that fallback.
|
|
330
|
+
if ! kubectl -n kube-system exec "${etcd_pod}" -c etcd -- \
|
|
331
|
+
etcdutl snapshot status "${snapshot_path}" --write-out=table; then
|
|
332
|
+
log_warn "etcdutl is unavailable; validating the snapshot with etcdctl."
|
|
333
|
+
kubectl -n kube-system exec "${etcd_pod}" -c etcd -- \
|
|
334
|
+
etcdctl snapshot status "${snapshot_path}" --write-out=table
|
|
335
|
+
fi
|
|
336
|
+
chmod 0600 "${snapshot_path}"
|
|
337
|
+
snapshot_size="$(stat -c '%s' "${snapshot_path}")"
|
|
338
|
+
log_info "Verified etcd snapshot (${snapshot_size} bytes)."
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
wait_for_statefulset_zero() {
|
|
342
|
+
local namespace="$1" name="$2" deadline counts
|
|
343
|
+
deadline=$((SECONDS + 600))
|
|
344
|
+
|
|
345
|
+
while (( SECONDS < deadline )); do
|
|
346
|
+
if ! counts="$(kubectl -n "${namespace}" get statefulset "${name}" \
|
|
347
|
+
-o go-template='{{if .status.replicas}}{{.status.replicas}}{{else}}0{{end}} {{if .status.readyReplicas}}{{.status.readyReplicas}}{{else}}0{{end}}')"; then
|
|
348
|
+
die "Failed to read StatefulSet status for ${namespace}/${name}."
|
|
349
|
+
fi
|
|
350
|
+
if [[ "${counts}" == "0 0" ]]; then
|
|
351
|
+
return 0
|
|
352
|
+
fi
|
|
353
|
+
sleep 2
|
|
354
|
+
done
|
|
355
|
+
|
|
356
|
+
die "Timed out waiting for StatefulSet ${namespace}/${name} to terminate (current/ready: ${counts})."
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
# Print PDB/pod triples for pods on this node that currently have no voluntary
|
|
360
|
+
# disruptions available. A stable singleton Deployment with such a PDB (for
|
|
361
|
+
# example metrics-server with minAvailable: 1) otherwise makes drain retry until
|
|
362
|
+
# its timeout: no replacement is created until the old Pod can be evicted.
|
|
363
|
+
find_blocking_pdb_pods() {
|
|
364
|
+
local pdb_json pods_json
|
|
365
|
+
pdb_json="$(mktemp "${STATE_DIR}/.pdbs.XXXXXX.json")"
|
|
366
|
+
pods_json="$(mktemp "${STATE_DIR}/.pods.XXXXXX.json")"
|
|
367
|
+
|
|
368
|
+
if ! kubectl get poddisruptionbudgets --all-namespaces -o json > "${pdb_json}"; then
|
|
369
|
+
rm -f -- "${pdb_json}" "${pods_json}"
|
|
370
|
+
die "Failed to inspect PodDisruptionBudgets before drain."
|
|
371
|
+
fi
|
|
372
|
+
if ! kubectl get pods --all-namespaces \
|
|
373
|
+
--field-selector "spec.nodeName=${KUBE_NODE_NAME}" -o json > "${pods_json}"; then
|
|
374
|
+
rm -f -- "${pdb_json}" "${pods_json}"
|
|
375
|
+
die "Failed to inspect pods on node ${KUBE_NODE_NAME} before drain."
|
|
376
|
+
fi
|
|
377
|
+
|
|
378
|
+
"${NODE_BIN}" - "${pdb_json}" "${pods_json}" <<'NODE'
|
|
379
|
+
const fs = require('fs');
|
|
380
|
+
const pdbs = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')).items || [];
|
|
381
|
+
const pods = JSON.parse(fs.readFileSync(process.argv[3], 'utf8')).items || [];
|
|
382
|
+
|
|
383
|
+
function matches(selector = {}, labels = {}) {
|
|
384
|
+
for (const [key, value] of Object.entries(selector.matchLabels || {})) {
|
|
385
|
+
if (labels[key] !== value) return false;
|
|
386
|
+
}
|
|
387
|
+
for (const expression of selector.matchExpressions || []) {
|
|
388
|
+
const present = Object.prototype.hasOwnProperty.call(labels, expression.key);
|
|
389
|
+
const values = expression.values || [];
|
|
390
|
+
if (expression.operator === 'In' && (!present || !values.includes(labels[expression.key]))) return false;
|
|
391
|
+
if (expression.operator === 'NotIn' && present && values.includes(labels[expression.key])) return false;
|
|
392
|
+
if (expression.operator === 'Exists' && !present) return false;
|
|
393
|
+
if (expression.operator === 'DoesNotExist' && present) return false;
|
|
394
|
+
}
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
for (const pdb of pdbs) {
|
|
399
|
+
if (pdb.status?.observedGeneration !== pdb.metadata?.generation) continue;
|
|
400
|
+
if ((pdb.status?.disruptionsAllowed ?? 0) !== 0) continue;
|
|
401
|
+
for (const pod of pods) {
|
|
402
|
+
if (pod.metadata?.namespace !== pdb.metadata?.namespace) continue;
|
|
403
|
+
if (pod.metadata?.deletionTimestamp) continue;
|
|
404
|
+
if (!matches(pdb.spec?.selector || {}, pod.metadata?.labels || {})) continue;
|
|
405
|
+
process.stdout.write(`${pdb.metadata.namespace}\t${pdb.metadata.name}\t${pod.metadata.name}\n`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
NODE
|
|
409
|
+
local node_status=$?
|
|
410
|
+
rm -f -- "${pdb_json}" "${pods_json}"
|
|
411
|
+
return "${node_status}"
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
# Resolve a Pod's immediate owner to the scalable top-level workload. The
|
|
415
|
+
# common Deployment -> ReplicaSet -> Pod ownership chain is followed explicitly.
|
|
416
|
+
resolve_pod_controller() {
|
|
417
|
+
local namespace="$1" pod="$2" kind name parent_kind parent_name owner_text
|
|
418
|
+
|
|
419
|
+
if ! owner_text="$(kubectl -n "${namespace}" get pod "${pod}" \
|
|
420
|
+
-o go-template='{{range .metadata.ownerReferences}}{{if .controller}}{{.kind}} {{.name}}{{end}}{{end}}')"; then
|
|
421
|
+
return 1
|
|
422
|
+
fi
|
|
423
|
+
read -r kind name <<< "${owner_text}"
|
|
424
|
+
[[ -n "${kind:-}" && -n "${name:-}" ]] || return 1
|
|
425
|
+
|
|
426
|
+
if [[ "${kind}" == "ReplicaSet" ]]; then
|
|
427
|
+
if ! owner_text="$(kubectl -n "${namespace}" get replicaset "${name}" \
|
|
428
|
+
-o go-template='{{range .metadata.ownerReferences}}{{if .controller}}{{.kind}} {{.name}}{{end}}{{end}}')"; then
|
|
429
|
+
return 1
|
|
430
|
+
fi
|
|
431
|
+
read -r parent_kind parent_name <<< "${owner_text}"
|
|
432
|
+
if [[ "${parent_kind:-}" == "Deployment" && -n "${parent_name:-}" ]]; then
|
|
433
|
+
kind="${parent_kind}"
|
|
434
|
+
name="${parent_name}"
|
|
435
|
+
fi
|
|
436
|
+
fi
|
|
437
|
+
|
|
438
|
+
printf '%s\t%s\n' "${kind}" "${name}"
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
wait_for_controller_zero() {
|
|
442
|
+
local kind="$1" namespace="$2" name="$3" deadline counts
|
|
443
|
+
deadline=$((SECONDS + 600))
|
|
444
|
+
while (( SECONDS < deadline )); do
|
|
445
|
+
if ! counts="$(kubectl -n "${namespace}" get "${kind}" "${name}" \
|
|
446
|
+
-o go-template='{{if .status.replicas}}{{.status.replicas}}{{else}}0{{end}} {{if .status.readyReplicas}}{{.status.readyReplicas}}{{else}}0{{end}}')"; then
|
|
447
|
+
die "Failed to read ${kind} status for ${namespace}/${name}."
|
|
448
|
+
fi
|
|
449
|
+
[[ "${counts}" == "0 0" ]] && return 0
|
|
450
|
+
sleep 2
|
|
451
|
+
done
|
|
452
|
+
die "Timed out waiting for ${kind} ${namespace}/${name} to terminate (replicas/ready: ${counts})."
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
wait_for_controller_ready() {
|
|
456
|
+
local kind="$1" namespace="$2" name="$3" desired="$4" deadline counts
|
|
457
|
+
deadline=$((SECONDS + 900))
|
|
458
|
+
while (( SECONDS < deadline )); do
|
|
459
|
+
if ! counts="$(kubectl -n "${namespace}" get "${kind}" "${name}" \
|
|
460
|
+
-o go-template='{{if .status.replicas}}{{.status.replicas}}{{else}}0{{end}} {{if .status.readyReplicas}}{{.status.readyReplicas}}{{else}}0{{end}}')"; then
|
|
461
|
+
die "Failed to read restored ${kind} status for ${namespace}/${name}."
|
|
462
|
+
fi
|
|
463
|
+
[[ "${counts}" == "${desired} ${desired}" ]] && return 0
|
|
464
|
+
sleep 5
|
|
465
|
+
done
|
|
466
|
+
die "Timed out waiting for restored ${kind} ${namespace}/${name} (replicas/ready: ${counts}, desired: ${desired})."
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
# PDBs remain enabled and unmodified. Intentionally scaling a blocking workload
|
|
470
|
+
# uses its controller's graceful Pod termination path and is safer than forcing
|
|
471
|
+
# drain with --disable-eviction. Its desired replicas are restored after boot.
|
|
472
|
+
quiesce_pdb_blockers() {
|
|
473
|
+
local namespace pdb pod owner kind name replicas record
|
|
474
|
+
local blockers=""
|
|
475
|
+
|
|
476
|
+
if ! blockers="$(find_blocking_pdb_pods)"; then
|
|
477
|
+
die "Failed to resolve workloads protected by PodDisruptionBudgets."
|
|
478
|
+
fi
|
|
479
|
+
[[ -n "${blockers}" ]] || {
|
|
480
|
+
log_info "No immediately blocking PodDisruptionBudgets found on ${KUBE_NODE_NAME}."
|
|
481
|
+
return 0
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
while IFS=$'\t' read -r namespace pdb pod; do
|
|
485
|
+
[[ -n "${namespace}" && -n "${pod}" ]] || continue
|
|
486
|
+
if ! owner="$(resolve_pod_controller "${namespace}" "${pod}")"; then
|
|
487
|
+
die "PDB ${namespace}/${pdb} blocks pod ${pod}, whose scalable controller could not be resolved."
|
|
488
|
+
fi
|
|
489
|
+
IFS=$'\t' read -r kind name <<< "${owner}"
|
|
490
|
+
|
|
491
|
+
case "${kind}" in
|
|
492
|
+
DaemonSet)
|
|
493
|
+
# kubectl drain already excludes DaemonSets.
|
|
494
|
+
continue
|
|
495
|
+
;;
|
|
496
|
+
Deployment|ReplicaSet|ReplicationController)
|
|
497
|
+
;;
|
|
498
|
+
StatefulSet)
|
|
499
|
+
# StatefulSets were already intentionally scaled to zero.
|
|
500
|
+
continue
|
|
501
|
+
;;
|
|
502
|
+
*)
|
|
503
|
+
die "PDB ${namespace}/${pdb} blocks unsupported controller ${kind}/${name} for pod ${pod}."
|
|
504
|
+
;;
|
|
505
|
+
esac
|
|
506
|
+
|
|
507
|
+
record="${kind}"$'\t'"${namespace}"$'\t'"${name}"$'\t'
|
|
508
|
+
if grep -Fq -- "${record}" "${CONTROLLER_STATE_FILE}"; then
|
|
509
|
+
continue
|
|
510
|
+
fi
|
|
511
|
+
if ! replicas="$(kubectl -n "${namespace}" get "${kind}" "${name}" -o jsonpath='{.spec.replicas}')"; then
|
|
512
|
+
die "Failed to read replicas for PDB-blocking ${kind} ${namespace}/${name}."
|
|
513
|
+
fi
|
|
514
|
+
[[ "${replicas}" =~ ^[0-9]+$ ]] || die "Invalid replica count for ${kind} ${namespace}/${name}."
|
|
515
|
+
|
|
516
|
+
# Persist before mutation so rollback and post-boot recovery are safe.
|
|
517
|
+
printf '%s\t%s\t%s\t%s\n' "${kind}" "${namespace}" "${name}" "${replicas}" \
|
|
518
|
+
>> "${CONTROLLER_STATE_FILE}"
|
|
519
|
+
log_warn "PDB ${namespace}/${pdb} cannot currently allow eviction; gracefully scaling ${kind} ${namespace}/${name} from ${replicas} to 0."
|
|
520
|
+
kubectl -n "${namespace}" scale "${kind}" "${name}" --replicas=0
|
|
521
|
+
done <<< "${blockers}"
|
|
522
|
+
|
|
523
|
+
while IFS=$'\t' read -r kind namespace name replicas; do
|
|
524
|
+
[[ -n "${kind}" && -n "${namespace}" && -n "${name}" ]] || continue
|
|
525
|
+
(( replicas > 0 )) && wait_for_controller_zero "${kind}" "${namespace}" "${name}"
|
|
526
|
+
done < "${CONTROLLER_STATE_FILE}"
|
|
527
|
+
return 0
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
scale_down_statefulsets() {
|
|
531
|
+
local namespace name replicas count=0
|
|
532
|
+
|
|
533
|
+
while IFS=$'\t' read -r namespace name replicas; do
|
|
534
|
+
[[ -n "${namespace}" && -n "${name}" ]] || continue
|
|
535
|
+
if (( replicas > 0 )); then
|
|
536
|
+
log_info "Gracefully scaling StatefulSet ${namespace}/${name} from ${replicas} to 0."
|
|
537
|
+
kubectl -n "${namespace}" scale statefulset "${name}" --replicas=0
|
|
538
|
+
if [[ "${name}" == "mongodb" ]]; then
|
|
539
|
+
printf '%s\n' "${namespace}" > "${MONGODB_QUIESCED_MARKER}"
|
|
540
|
+
chmod 0600 "${MONGODB_QUIESCED_MARKER}"
|
|
541
|
+
fi
|
|
542
|
+
((count += 1))
|
|
543
|
+
fi
|
|
544
|
+
done < "${STATE_FILE}"
|
|
545
|
+
|
|
546
|
+
while IFS=$'\t' read -r namespace name replicas; do
|
|
547
|
+
[[ -n "${namespace}" && -n "${name}" ]] || continue
|
|
548
|
+
(( replicas > 0 )) && wait_for_statefulset_zero "${namespace}" "${name}"
|
|
549
|
+
done < "${STATE_FILE}"
|
|
550
|
+
|
|
551
|
+
log_info "All ${count} active non-system StatefulSet(s) terminated cleanly."
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
drain_node() {
|
|
555
|
+
log_info "Draining ${KUBE_NODE_NAME}; PodDisruptionBudgets remain enforced."
|
|
556
|
+
kubectl drain "${KUBE_NODE_NAME}" \
|
|
557
|
+
--ignore-daemonsets \
|
|
558
|
+
--delete-emptydir-data \
|
|
559
|
+
--force \
|
|
560
|
+
--grace-period=60 \
|
|
561
|
+
--timeout=15m
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
sync_storage() {
|
|
565
|
+
log_info "Flushing kernel filesystem caches to persistent storage."
|
|
566
|
+
sync
|
|
567
|
+
sync
|
|
568
|
+
sync
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
detect_active_runtimes() {
|
|
572
|
+
local service
|
|
573
|
+
ACTIVE_RUNTIMES=()
|
|
574
|
+
# Stop socket activation before Docker, and stop Docker before the shared
|
|
575
|
+
# containerd daemon it may depend on. Rollback starts this list in reverse.
|
|
576
|
+
for service in docker.socket docker.service crio.service containerd.service; do
|
|
577
|
+
if systemctl is-active --quiet "${service}"; then
|
|
578
|
+
ACTIVE_RUNTIMES+=("${service}")
|
|
579
|
+
fi
|
|
580
|
+
done
|
|
581
|
+
if (( ${#ACTIVE_RUNTIMES[@]} == 0 )); then
|
|
582
|
+
die "No active supported container runtime found (checked CRI-O, containerd, and Docker)."
|
|
583
|
+
fi
|
|
584
|
+
log_info "Active container runtime unit(s): ${ACTIVE_RUNTIMES[*]}"
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
stop_kubernetes_services() {
|
|
588
|
+
local service
|
|
589
|
+
detect_active_runtimes
|
|
590
|
+
|
|
591
|
+
# Stop kubelet first so it cannot recreate containers while the CRI runtime
|
|
592
|
+
# is shutting down. systemd will finish terminating each unit synchronously.
|
|
593
|
+
log_info "Stopping kubelet."
|
|
594
|
+
systemctl stop kubelet.service
|
|
595
|
+
SERVICES_STOPPED=1
|
|
596
|
+
|
|
597
|
+
for service in "${ACTIVE_RUNTIMES[@]}"; do
|
|
598
|
+
log_info "Stopping container runtime ${service}."
|
|
599
|
+
systemctl stop "${service}"
|
|
600
|
+
done
|
|
601
|
+
|
|
602
|
+
if systemctl is-active --quiet kubelet.service; then
|
|
603
|
+
die "kubelet is still active after stop."
|
|
604
|
+
fi
|
|
605
|
+
for service in "${ACTIVE_RUNTIMES[@]}"; do
|
|
606
|
+
if systemctl is-active --quiet "${service}"; then
|
|
607
|
+
die "${service} is still active after stop."
|
|
608
|
+
fi
|
|
609
|
+
done
|
|
610
|
+
# systemctl uses exit status 3 for an inactive unit. Reaching this point is
|
|
611
|
+
# successful verification, so never leak that expected status to set -e.
|
|
612
|
+
return 0
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
wait_for_api() {
|
|
616
|
+
local attempt
|
|
617
|
+
for ((attempt = 1; attempt <= 60; attempt++)); do
|
|
618
|
+
if kubectl version --request-timeout=5s >/dev/null 2>&1; then
|
|
619
|
+
return 0
|
|
620
|
+
fi
|
|
621
|
+
(( attempt % 6 == 0 )) && log_warn "Waiting for Kubernetes API (${attempt}/60)."
|
|
622
|
+
sleep 5
|
|
623
|
+
done
|
|
624
|
+
die "Kubernetes API did not become reachable within five minutes."
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
wait_for_node_ready() {
|
|
628
|
+
local node="$1" attempt status
|
|
629
|
+
for ((attempt = 1; attempt <= 120; attempt++)); do
|
|
630
|
+
status="$(kubectl get node "${node}" -o jsonpath='{range .status.conditions[?(@.type=="Ready")]}{.status}{end}' 2>/dev/null || true)"
|
|
631
|
+
[[ "${status}" == "True" ]] && return 0
|
|
632
|
+
(( attempt % 12 == 0 )) && log_warn "Waiting for node ${node} to become Ready (${attempt}/120)."
|
|
633
|
+
sleep 5
|
|
634
|
+
done
|
|
635
|
+
die "Node ${node} did not become Ready within ten minutes."
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
# Event rules are restored only after the controllers that drain displaced have
|
|
639
|
+
# converged. This closes the gap where the node is Ready but ingress, exporters,
|
|
640
|
+
# or application Deployments are still starting and would immediately alert.
|
|
641
|
+
wait_for_cluster_rollouts() {
|
|
642
|
+
local resource namespace name resources
|
|
643
|
+
for resource in deployment daemonset statefulset; do
|
|
644
|
+
if ! resources="$(kubectl get "${resource}" --all-namespaces \
|
|
645
|
+
-o go-template='{{range .items}}{{.metadata.namespace}}{{"\t"}}{{.metadata.name}}{{"\n"}}{{end}}')"; then
|
|
646
|
+
die "Failed to list ${resource} resources before event resynchronization."
|
|
647
|
+
fi
|
|
648
|
+
while IFS=$'\t' read -r namespace name; do
|
|
649
|
+
[[ -n "${namespace}" && -n "${name}" ]] || continue
|
|
650
|
+
log_info "Waiting for ${resource} rollout ${namespace}/${name} before restoring events."
|
|
651
|
+
kubectl -n "${namespace}" rollout status "${resource}/${name}" --timeout=15m
|
|
652
|
+
done <<< "${resources}"
|
|
653
|
+
done
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
# MongoDB needs its Kubernetes Secrets reconciled and its replica-set membership
|
|
657
|
+
# checked after pods and storage are back. Phase markers make retries idempotent:
|
|
658
|
+
# a later event-resync failure must not repeat the forced secret onboarding.
|
|
659
|
+
restore_mongodb_runtime() {
|
|
660
|
+
local event_engine_root mongodb_namespace
|
|
661
|
+
[[ -s "${MONGODB_QUIESCED_MARKER}" ]] || return 0
|
|
662
|
+
IFS= read -r mongodb_namespace < "${MONGODB_QUIESCED_MARKER}"
|
|
663
|
+
[[ -n "${mongodb_namespace}" ]] || die "MongoDB recovery namespace is empty."
|
|
664
|
+
[[ -s "${ENGINE_ROOT_FILE}" ]] || die "Engine-root recovery state is missing: ${ENGINE_ROOT_FILE}"
|
|
665
|
+
IFS= read -r event_engine_root < "${ENGINE_ROOT_FILE}"
|
|
666
|
+
[[ -d "${event_engine_root}" ]] || die "Saved engine root is unavailable: ${event_engine_root}"
|
|
667
|
+
|
|
668
|
+
if [[ ! -e "${MONGODB_SECRETS_MARKER}" ]]; then
|
|
669
|
+
log_info "MongoDB was active before shutdown; reconciling its SOPS-managed secrets."
|
|
670
|
+
(
|
|
671
|
+
cd "${event_engine_root}"
|
|
672
|
+
# The secret workflow is a top-level CLI command; `run secret` is
|
|
673
|
+
# not a registered runner and would fail before doing any setup.
|
|
674
|
+
"${NODE_BIN}" bin secret --setup --force --namespace "${mongodb_namespace}"
|
|
675
|
+
)
|
|
676
|
+
: > "${MONGODB_SECRETS_MARKER}"
|
|
677
|
+
chmod 0600 "${MONGODB_SECRETS_MARKER}"
|
|
678
|
+
fi
|
|
679
|
+
|
|
680
|
+
if [[ ! -e "${MONGODB_REPLICA_MARKER}" ]]; then
|
|
681
|
+
log_info "Reinitializing the recovered MongoDB replica-set membership."
|
|
682
|
+
(
|
|
683
|
+
cd "${event_engine_root}"
|
|
684
|
+
"${NODE_BIN}" bin run restore-mongo --kubeadm --namespace "${mongodb_namespace}"
|
|
685
|
+
)
|
|
686
|
+
: > "${MONGODB_REPLICA_MARKER}"
|
|
687
|
+
chmod 0600 "${MONGODB_REPLICA_MARKER}"
|
|
688
|
+
fi
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
restore_statefulsets() {
|
|
692
|
+
local kind namespace name replicas node
|
|
693
|
+
|
|
694
|
+
require_root
|
|
695
|
+
require_command kubectl
|
|
696
|
+
require_command readlink
|
|
697
|
+
resolve_node_binary
|
|
698
|
+
[[ -s "${NODE_FILE}" ]] || die "Restore node state is missing: ${NODE_FILE}"
|
|
699
|
+
[[ -e "${STATE_FILE}" ]] || die "StatefulSet restore state is missing: ${STATE_FILE}"
|
|
700
|
+
IFS= read -r node < "${NODE_FILE}"
|
|
701
|
+
[[ -n "${node}" ]] || die "Restore node name is empty."
|
|
702
|
+
|
|
703
|
+
resolve_kubeconfig
|
|
704
|
+
wait_for_api
|
|
705
|
+
wait_for_node_ready "${node}"
|
|
706
|
+
|
|
707
|
+
if [[ -e "${UNCORDON_MARKER}" ]]; then
|
|
708
|
+
log_info "Restoring schedulability for node ${node}."
|
|
709
|
+
kubectl uncordon "${node}"
|
|
710
|
+
else
|
|
711
|
+
log_warn "Node ${node} was cordoned before shutdown and will remain cordoned."
|
|
712
|
+
fi
|
|
713
|
+
|
|
714
|
+
if [[ -e "${CONTROLLER_STATE_FILE}" ]]; then
|
|
715
|
+
while IFS=$'\t' read -r kind namespace name replicas; do
|
|
716
|
+
[[ -n "${kind}" && -n "${namespace}" && -n "${name}" ]] || continue
|
|
717
|
+
[[ "${replicas}" =~ ^[0-9]+$ ]] || die "Invalid controller replica count in ${CONTROLLER_STATE_FILE}."
|
|
718
|
+
log_info "Restoring ${kind} ${namespace}/${name} to ${replicas} replica(s)."
|
|
719
|
+
kubectl -n "${namespace}" scale "${kind}" "${name}" --replicas="${replicas}"
|
|
720
|
+
done < "${CONTROLLER_STATE_FILE}"
|
|
721
|
+
|
|
722
|
+
while IFS=$'\t' read -r kind namespace name replicas; do
|
|
723
|
+
[[ -n "${kind}" && -n "${namespace}" && -n "${name}" ]] || continue
|
|
724
|
+
(( replicas == 0 )) && continue
|
|
725
|
+
wait_for_controller_ready "${kind}" "${namespace}" "${name}" "${replicas}"
|
|
726
|
+
done < "${CONTROLLER_STATE_FILE}"
|
|
727
|
+
fi
|
|
728
|
+
|
|
729
|
+
while IFS=$'\t' read -r namespace name replicas; do
|
|
730
|
+
[[ -n "${namespace}" && -n "${name}" ]] || continue
|
|
731
|
+
[[ "${replicas}" =~ ^[0-9]+$ ]] || die "Invalid replica count in ${STATE_FILE}."
|
|
732
|
+
log_info "Restoring StatefulSet ${namespace}/${name} to ${replicas} replica(s)."
|
|
733
|
+
kubectl -n "${namespace}" scale statefulset "${name}" --replicas="${replicas}"
|
|
734
|
+
done < "${STATE_FILE}"
|
|
735
|
+
|
|
736
|
+
while IFS=$'\t' read -r namespace name replicas; do
|
|
737
|
+
[[ -n "${namespace}" && -n "${name}" ]] || continue
|
|
738
|
+
(( replicas == 0 )) && continue
|
|
739
|
+
kubectl -n "${namespace}" rollout status "statefulset/${name}" --timeout=15m
|
|
740
|
+
done < "${STATE_FILE}"
|
|
741
|
+
|
|
742
|
+
# Alerting is the final component restored: deliberate pod absence must not
|
|
743
|
+
# be observed as a fault while controllers and databases are still starting.
|
|
744
|
+
wait_for_cluster_rollouts
|
|
745
|
+
restore_mongodb_runtime
|
|
746
|
+
resume_events
|
|
747
|
+
|
|
748
|
+
rm -f -- "${STATE_FILE}" "${CONTROLLER_STATE_FILE}" "${NODE_FILE}" "${KUBECONFIG_FILE}" \
|
|
749
|
+
"${ENGINE_ROOT_FILE}" "${NODE_BIN_FILE}" "${MONGODB_QUIESCED_MARKER}" "${MONGODB_SECRETS_MARKER}" \
|
|
750
|
+
"${MONGODB_REPLICA_MARKER}" "${UNCORDON_MARKER}"
|
|
751
|
+
log_info "Post-boot Kubernetes restoration completed successfully."
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
rollback_shutdown() {
|
|
755
|
+
local service index rollback_status=0
|
|
756
|
+
(( ROLLBACK_RUNNING == 0 )) || return 0
|
|
757
|
+
ROLLBACK_RUNNING=1
|
|
758
|
+
set +e
|
|
759
|
+
log_warn "Attempting to return the node and workloads to their pre-shutdown state."
|
|
760
|
+
|
|
761
|
+
if (( SERVICES_STOPPED == 1 )); then
|
|
762
|
+
# Dependencies come up in the inverse order used during shutdown.
|
|
763
|
+
for ((index = ${#ACTIVE_RUNTIMES[@]} - 1; index >= 0; index--)); do
|
|
764
|
+
service="${ACTIVE_RUNTIMES[index]}"
|
|
765
|
+
systemctl start "${service}" || rollback_status=1
|
|
766
|
+
done
|
|
767
|
+
systemctl start kubelet.service || rollback_status=1
|
|
768
|
+
wait_for_api || rollback_status=1
|
|
769
|
+
fi
|
|
770
|
+
|
|
771
|
+
if (( WORKLOAD_STATE_SAVED == 1 )); then
|
|
772
|
+
(set -e; restore_statefulsets) || rollback_status=1
|
|
773
|
+
elif (( CORDONED_BY_SCRIPT == 1 )); then
|
|
774
|
+
kubectl uncordon "${KUBE_NODE_NAME}" || rollback_status=1
|
|
775
|
+
fi
|
|
776
|
+
set -e
|
|
777
|
+
return "${rollback_status}"
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
on_error() {
|
|
781
|
+
local exit_code=$? line="$1"
|
|
782
|
+
trap - ERR INT TERM
|
|
783
|
+
log_error "Shutdown preparation failed at line ${line} (exit ${exit_code}); the machine will NOT be powered off."
|
|
784
|
+
rollback_shutdown || log_error "Automatic rollback was incomplete; use '${RESTORE_BIN} --restore-statefulsets' after correcting API/service availability."
|
|
785
|
+
exit "${exit_code}"
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
on_signal() {
|
|
789
|
+
local signal="$1"
|
|
790
|
+
trap - ERR INT TERM
|
|
791
|
+
log_error "Received ${signal}; the machine will NOT be powered off."
|
|
792
|
+
rollback_shutdown || true
|
|
793
|
+
exit 130
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
graceful_shutdown() {
|
|
797
|
+
check_prerequisites
|
|
798
|
+
resolve_node_environment
|
|
799
|
+
prepare_post_boot_restore
|
|
800
|
+
snapshot_etcd
|
|
801
|
+
suspend_events
|
|
802
|
+
cordon_node
|
|
803
|
+
scale_down_statefulsets
|
|
804
|
+
quiesce_pdb_blockers
|
|
805
|
+
drain_node
|
|
806
|
+
sync_storage
|
|
807
|
+
stop_kubernetes_services
|
|
808
|
+
sync_storage
|
|
809
|
+
|
|
810
|
+
log_info "============================================================"
|
|
811
|
+
log_info "GRACEFUL SHUTDOWN COMPLETE: ${KUBE_NODE_NAME} is safe to power off."
|
|
812
|
+
log_info "StatefulSets will be restored automatically after the next boot."
|
|
813
|
+
log_info "============================================================"
|
|
814
|
+
|
|
815
|
+
# Use the standard systemd-backed halt path so remaining filesystems and
|
|
816
|
+
# services receive their normal shutdown ordering.
|
|
817
|
+
if ! shutdown -h now; then
|
|
818
|
+
die "The operating-system shutdown command failed."
|
|
819
|
+
fi
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
main() {
|
|
823
|
+
case "${1:-}" in
|
|
824
|
+
"")
|
|
825
|
+
graceful_shutdown
|
|
826
|
+
;;
|
|
827
|
+
--restore-statefulsets)
|
|
828
|
+
[[ $# -eq 1 ]] || die "--restore-statefulsets takes no additional arguments."
|
|
829
|
+
restore_statefulsets
|
|
830
|
+
;;
|
|
831
|
+
-h|--help)
|
|
832
|
+
printf 'Usage: sudo %s [--restore-statefulsets]\n' "${PROGRAM}"
|
|
833
|
+
;;
|
|
834
|
+
*)
|
|
835
|
+
die "Unknown argument: $1"
|
|
836
|
+
;;
|
|
837
|
+
esac
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
trap 'on_error ${LINENO}' ERR
|
|
841
|
+
trap 'on_signal INT' INT
|
|
842
|
+
trap 'on_signal TERM' TERM
|
|
843
|
+
main "$@"
|