@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.
Files changed (471) hide show
  1. package/.dockerignore +1 -0
  2. package/.github/workflows/coverall.cyberia.ci.yml +34 -14
  3. package/.github/workflows/cyberia-client.cd.yml +5 -0
  4. package/.github/workflows/cyberia-server.cd.yml +5 -0
  5. package/.github/workflows/docker-image.cyberia-client.ci.yml +8 -5
  6. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +8 -3
  7. package/.github/workflows/docker-image.cyberia-server.ci.yml +8 -5
  8. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +8 -5
  9. package/.github/workflows/docker-image.engine-cyberia.ci.yml +15 -1
  10. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +15 -1
  11. package/.github/workflows/engine-cyberia.cd.yml +3 -0
  12. package/.github/workflows/engine-cyberia.ci.yml +4 -5
  13. package/.github/workflows/ghpkg.ci.yml +35 -20
  14. package/.github/workflows/hardhat.ci.yml +12 -11
  15. package/.github/workflows/npmpkg.ci.yml +17 -15
  16. package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
  17. package/.github/workflows/release.cd.yml +1 -0
  18. package/.prettierignore +1 -0
  19. package/AGENTS.md +47 -0
  20. package/CHANGELOG.md +565 -2510
  21. package/CLI-HELP.md +306 -147
  22. package/Dockerfile +25 -9
  23. package/Dockerfile.dev +25 -9
  24. package/Dockerfile.test +4 -4
  25. package/README.md +21 -1
  26. package/bin/build.js +66 -41
  27. package/bin/build.template.js +8 -3
  28. package/bin/cyberia.js +308 -91
  29. package/bin/deploy.js +18 -104
  30. package/bin/index.js +308 -91
  31. package/conf.js +91 -4
  32. package/deploy/cyberia-client/deploy.sh +47 -0
  33. package/deploy/cyberia-client/package.sh +18 -0
  34. package/deploy/cyberia-client/state.sh +14 -0
  35. package/deploy/cyberia-server/deploy.sh +47 -0
  36. package/deploy/cyberia-server/package.sh +18 -0
  37. package/deploy/cyberia-server/state.sh +14 -0
  38. package/deploy/dd-cyberia/deploy.sh +6 -26
  39. package/deploy/dd-cyberia/init.sh +35 -43
  40. package/deploy/dd-cyberia/package.sh +17 -0
  41. package/deploy/dd-cyberia/state.sh +14 -0
  42. package/deploy/dd-cyberia/sync-deploy.sh +92 -170
  43. package/deploy/lib/github-actions-logging.sh +594 -0
  44. package/deploy/lib/host.sh +185 -0
  45. package/deploy/lib/state.sh +62 -0
  46. package/deploy/pwa-microservices-template/deploy.sh +12 -39
  47. package/deploy/release/deploy.sh +9 -39
  48. package/deployment.yaml +7 -2
  49. package/docker-compose.yml +7 -7
  50. package/examples/static-page/README.md +19 -19
  51. package/examples/static-page/ssr-components/CustomPage.js +2 -2
  52. package/hardhat/hardhat.config.js +1 -6
  53. package/hardhat/package-lock.json +101 -40
  54. package/hardhat/package.json +2 -2
  55. package/manifests/alertmanager/deployment.yaml +81 -0
  56. package/manifests/blackbox-exporter/deployment.yaml +75 -0
  57. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +32 -7
  58. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +32 -7
  59. package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
  60. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  61. package/manifests/grafana/deployment.yaml +26 -7
  62. package/manifests/grafana/kustomization.yaml +1 -0
  63. package/manifests/grafana/service.yaml +6 -1
  64. package/manifests/mariadb/pv.yaml +1 -0
  65. package/manifests/mongodb/statefulset.yaml +9 -2
  66. package/manifests/mysql/pv-pvc.yaml +14 -1
  67. package/manifests/postgresql/pv.yaml +12 -1
  68. package/manifests/postgresql/pvc.yaml +1 -0
  69. package/manifests/prometheus/deployment.yaml +18 -22
  70. package/package.json +83 -10
  71. package/scripts/event-monitor.sh +56 -0
  72. package/scripts/k3s-node-setup.sh +27 -14
  73. package/scripts/kubeadm-node-setup.sh +31 -11
  74. package/scripts/maas-setup.sh +8 -8
  75. package/scripts/selinux-normalize.sh +907 -0
  76. package/scripts/shutdown-machine.sh +843 -0
  77. package/scripts/test-monitor.sh +3 -3
  78. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +1 -1
  79. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +2 -2
  80. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +2 -2
  81. package/src/api/core/core.controller.js +1 -1
  82. package/src/api/core/core.router.js +2 -2
  83. package/src/api/core/core.service.js +2 -2
  84. package/src/api/crypto/crypto.controller.js +1 -1
  85. package/src/api/crypto/crypto.service.js +1 -1
  86. package/src/api/cyberia-action/cyberia-action.controller.js +1 -1
  87. package/src/api/cyberia-action/cyberia-action.router.js +1 -1
  88. package/src/api/cyberia-action/cyberia-action.service.js +2 -2
  89. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -1
  90. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +1 -1
  91. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +2 -2
  92. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
  93. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  94. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +2 -2
  95. package/src/api/cyberia-entity/cyberia-entity.controller.js +1 -1
  96. package/src/api/cyberia-entity/cyberia-entity.router.js +1 -1
  97. package/src/api/cyberia-entity/cyberia-entity.service.js +2 -2
  98. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +1 -1
  99. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +1 -1
  100. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +2 -2
  101. package/src/api/cyberia-instance/cyberia-fallback-capture.js +466 -0
  102. package/src/api/cyberia-instance/cyberia-fallback-world.js +8 -0
  103. package/src/api/cyberia-instance/cyberia-instance-map.service.js +1 -1
  104. package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -1
  105. package/src/api/cyberia-instance/cyberia-instance.router.js +2 -2
  106. package/src/api/cyberia-instance/cyberia-instance.service.js +2 -2
  107. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
  108. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
  109. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +2 -2
  110. package/src/api/cyberia-map/cyberia-map.controller.js +1 -1
  111. package/src/api/cyberia-map/cyberia-map.router.js +1 -1
  112. package/src/api/cyberia-map/cyberia-map.service.js +2 -2
  113. package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
  114. package/src/api/cyberia-quest/cyberia-quest.router.js +1 -1
  115. package/src/api/cyberia-quest/cyberia-quest.service.js +2 -2
  116. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +1 -1
  117. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +1 -1
  118. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +2 -2
  119. package/src/api/cyberia-saga/cyberia-saga.controller.js +1 -1
  120. package/src/api/cyberia-saga/cyberia-saga.router.js +1 -1
  121. package/src/api/cyberia-saga/cyberia-saga.service.js +2 -2
  122. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +0 -26
  123. package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
  124. package/src/api/cyberia-skill/cyberia-skill.router.js +1 -1
  125. package/src/api/cyberia-skill/cyberia-skill.service.js +2 -2
  126. package/src/api/default/default.controller.js +1 -1
  127. package/src/api/default/default.router.js +1 -1
  128. package/src/api/default/default.service.js +2 -2
  129. package/src/api/document/document.controller.js +1 -1
  130. package/src/api/document/document.router.js +1 -1
  131. package/src/api/document/document.service.js +2 -2
  132. package/src/api/file/file.controller.js +1 -1
  133. package/src/api/file/file.router.js +1 -1
  134. package/src/api/file/file.service.js +2 -2
  135. package/src/api/instance/instance.controller.js +1 -1
  136. package/src/api/instance/instance.router.js +1 -1
  137. package/src/api/instance/instance.service.js +2 -2
  138. package/src/api/ipfs/ipfs.controller.js +1 -1
  139. package/src/api/ipfs/ipfs.router.js +2 -2
  140. package/src/api/ipfs/ipfs.service.js +2 -2
  141. package/src/api/object-layer/object-layer.controller.js +1 -1
  142. package/src/api/object-layer/object-layer.router.js +2 -2
  143. package/src/api/object-layer/object-layer.service.js +3 -3
  144. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +1 -1
  145. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +1 -1
  146. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +2 -2
  147. package/src/api/test/test.controller.js +1 -1
  148. package/src/api/test/test.service.js +1 -1
  149. package/src/api/user/guest.service.js +3 -3
  150. package/src/api/user/user.controller.js +1 -1
  151. package/src/api/user/user.router.js +2 -2
  152. package/src/api/user/user.service.js +4 -4
  153. package/src/api.js +5 -9
  154. package/src/cli/app.js +391 -0
  155. package/src/cli/baremetal.js +30 -35
  156. package/src/cli/client.js +306 -0
  157. package/src/cli/cloud-init.js +3 -3
  158. package/src/cli/cluster.js +184 -82
  159. package/src/cli/db.js +120 -68
  160. package/src/cli/deploy.js +153 -71
  161. package/src/cli/docker-compose.js +5 -5
  162. package/src/cli/domains.js +184 -0
  163. package/src/cli/dotenv-store.js +143 -0
  164. package/src/cli/event.js +2281 -0
  165. package/src/cli/fs.js +3 -3
  166. package/src/cli/host.js +672 -0
  167. package/src/cli/image.js +4 -6
  168. package/src/cli/index.js +322 -142
  169. package/src/cli/ipfs.js +34 -30
  170. package/src/cli/kickstart.js +1 -1
  171. package/src/cli/kubectl.js +218 -41
  172. package/src/cli/lxd.js +3 -3
  173. package/src/cli/monitor.js +1359 -26
  174. package/src/cli/package.js +90 -0
  175. package/src/cli/release.js +16 -12
  176. package/src/cli/repository.js +558 -363
  177. package/src/cli/run.js +331 -650
  178. package/src/cli/secrets.js +2158 -964
  179. package/src/cli/ssh.js +478 -279
  180. package/src/cli/state.js +394 -0
  181. package/src/cli/static.js +2 -2
  182. package/src/cli/system.js +1 -1
  183. package/src/cli/test.js +199 -125
  184. package/src/cli/vultr.js +61 -21
  185. package/src/cli/wireguard.js +1111 -306
  186. package/src/client/components/core/CalendarCore.js +53 -96
  187. package/src/client/components/core/Docs.js +258 -211
  188. package/src/client/components/core/FileExplorer.js +0 -21
  189. package/src/client/components/core/PanelForm.js +4 -4
  190. package/src/client/components/core/Repository.js +56 -0
  191. package/src/client/components/core/Translate.js +7 -0
  192. package/src/client/components/core/{FullScreen.js → ViewModeController.js} +42 -39
  193. package/src/client/components/core/Wallet.js +0 -11
  194. package/src/client/components/core/Worker.js +7 -1
  195. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +2 -2
  196. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +2 -1
  197. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
  198. package/src/client/components/default/AppShellDefault.js +1 -1
  199. package/src/client/components/default/SettingsDefault.js +2 -2
  200. package/src/client/components/itemledger/SettingsItemledger.js +2 -2
  201. package/src/client/components/underpost/AppShellUnderpost.js +3 -2
  202. package/src/client/components/underpost/SettingsUnderpost.js +2 -2
  203. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +32 -5
  204. package/src/client/public/cyberia-docs/CYBERIA.md +21 -1
  205. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +23 -6
  206. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  207. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  208. package/src/client/public/default/android-chrome-144x144.png +0 -0
  209. package/src/client/public/default/android-chrome-192x192.png +0 -0
  210. package/src/client/public/default/android-chrome-256x256.png +0 -0
  211. package/src/client/public/default/android-chrome-36x36.png +0 -0
  212. package/src/client/public/default/android-chrome-384x384.png +0 -0
  213. package/src/client/public/default/android-chrome-48x48.png +0 -0
  214. package/src/client/public/default/android-chrome-512x512.png +0 -0
  215. package/src/client/public/default/android-chrome-72x72.png +0 -0
  216. package/src/client/public/default/android-chrome-96x96.png +0 -0
  217. package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
  218. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  219. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  220. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  221. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  222. package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
  223. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  224. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  225. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  226. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  227. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  228. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  229. package/src/client/public/default/apple-touch-icon.png +0 -0
  230. package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
  231. package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
  232. package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
  233. package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
  234. package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
  235. package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
  236. package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
  237. package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
  238. package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
  239. package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
  240. package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
  241. package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
  242. package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
  243. package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
  244. package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
  245. package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
  246. package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
  247. package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
  248. package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
  249. package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
  250. package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
  251. package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
  252. package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
  253. package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
  254. package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
  255. package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
  256. package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
  257. package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
  258. package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
  259. package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
  260. package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
  261. package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
  262. package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
  263. package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
  264. package/src/client/public/default/assets/background/white.jpg +0 -0
  265. package/src/client/public/default/browserconfig.xml +1 -1
  266. package/src/client/public/default/favicon-16x16.png +0 -0
  267. package/src/client/public/default/favicon-32x32.png +0 -0
  268. package/src/client/public/default/favicon-48x48.png +0 -0
  269. package/src/client/public/default/favicon.ico +0 -0
  270. package/src/client/public/default/manifest.webmanifest +5 -5
  271. package/src/client/public/default/mstile-144x144.png +0 -0
  272. package/src/client/public/default/mstile-150x150.png +0 -0
  273. package/src/client/public/default/mstile-310x150.png +0 -0
  274. package/src/client/public/default/mstile-310x310.png +0 -0
  275. package/src/client/public/default/mstile-70x70.png +0 -0
  276. package/src/client/public/default/yandex-browser-50x50.png +0 -0
  277. package/src/client/public/default/yandex-browser-manifest.json +1 -1
  278. package/src/client/ssr/RootDocument.js +1 -1
  279. package/src/client/ssr/body/404.js +1 -1
  280. package/src/client/ssr/body/500.js +1 -1
  281. package/src/client/ssr/body/CacheControl.js +1 -1
  282. package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +1 -1
  283. package/src/client/ssr/body/DefaultSplashScreen.js +1 -1
  284. package/src/client/ssr/body/SwaggerDarkMode.js +1 -1
  285. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +1 -1
  286. package/src/client/ssr/head/CryptokoynScripts.js +1 -1
  287. package/src/client/ssr/head/Css.js +1 -1
  288. package/src/client/ssr/head/CyberiaPortalScripts.js +1 -1
  289. package/src/client/ssr/head/DefaultScripts.js +1 -1
  290. package/src/client/ssr/head/ItemledgerScripts.js +1 -1
  291. package/src/client/ssr/head/Microdata.js +1 -1
  292. package/src/client/ssr/head/Production.js +1 -1
  293. package/src/client/ssr/head/Pwa.js +1 -1
  294. package/src/client/ssr/head/PwaItemledger.js +1 -1
  295. package/src/client/ssr/head/Seo.js +1 -1
  296. package/src/client/ssr/head/UnderpostScripts.js +1 -1
  297. package/src/client/ssr/mailer/DefaultRecoverEmail.js +1 -1
  298. package/src/client/ssr/mailer/DefaultVerifyEmail.js +1 -1
  299. package/src/client/ssr/views/Cyberia404.js +7 -5
  300. package/src/client/ssr/views/CyberiaServerMetrics.js +1 -1
  301. package/src/client/ssr/views/Maintenance.js +1 -1
  302. package/src/client/ssr/views/NoNetworkConnection.js +1 -1
  303. package/src/client/ssr/views/Test.js +1 -1
  304. package/src/client/sw/core.sw.js +20 -14
  305. package/src/client-builder/client-build-docs.js +61 -46
  306. package/src/client-builder/client-build-live.js +2 -2
  307. package/src/client-builder/client-build.js +38 -30
  308. package/src/client-builder/client-dev-server.js +3 -3
  309. package/src/client-builder/client-icons.js +2 -2
  310. package/src/client-builder/ssr.js +3 -3
  311. package/src/client.build.js +3 -6
  312. package/src/client.dev.js +3 -6
  313. package/src/db/DataBaseProvider.js +186 -3
  314. package/src/db/mariadb/MariaDB.js +3 -2
  315. package/src/db/mongo/MongoBootstrap.js +273 -101
  316. package/src/db/mongo/MongooseDB.js +7 -8
  317. package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
  318. package/src/grpc/cyberia/grpc-server.js +1 -1
  319. package/src/index.js +79 -23
  320. package/src/mailer/EmailRender.js +2 -2
  321. package/src/mailer/MailerInterceptor.js +87 -0
  322. package/src/mailer/MailerProvider.js +50 -6
  323. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +1 -1
  324. package/src/projects/cyberia/besu-genesis-generator.js +3 -3
  325. package/src/projects/cyberia/catalog-cyberia.js +53 -10
  326. package/src/projects/cyberia/gemini-client.js +1 -1
  327. package/src/projects/cyberia/generate-saga.js +1 -1
  328. package/src/projects/cyberia/hot-reload-trigger.js +1 -1
  329. package/src/projects/cyberia/instance-data.js +1 -1
  330. package/src/projects/cyberia/ipfs-client.js +4 -4
  331. package/src/projects/cyberia/map-preview-generator.js +1 -1
  332. package/src/projects/cyberia/object-layer.js +1 -1
  333. package/src/projects/cyberia/semantic-layer-generator.js +1 -1
  334. package/src/projects/underpost/catalog-underpost.js +9 -1
  335. package/src/proxy.js +4 -7
  336. package/src/runtime/cyberia-client/Dockerfile +9 -4
  337. package/src/runtime/cyberia-client/Dockerfile.dev +8 -2
  338. package/src/runtime/cyberia-server/Dockerfile +9 -4
  339. package/src/runtime/cyberia-server/Dockerfile.dev +8 -2
  340. package/src/runtime/engine-cyberia/Dockerfile +25 -9
  341. package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
  342. package/src/runtime/engine-cyberia/Dockerfile.test +4 -4
  343. package/src/runtime/engine-cyberia/docker-compose.yml +7 -7
  344. package/src/runtime/express/Express.js +32 -27
  345. package/src/runtime/lampp/Lampp.js +22 -70
  346. package/src/runtime/nginx/Nginx.js +1 -1
  347. package/src/runtime/wp/Wp.js +16 -48
  348. package/src/server/{catalog.js → build/catalog.js} +15 -5
  349. package/src/server/build/coverage.js +117 -0
  350. package/src/server/build/execution.js +402 -0
  351. package/src/server/build/package.js +465 -0
  352. package/src/server/build/testing.js +528 -0
  353. package/src/server/{dns.js → network/dns.js} +144 -11
  354. package/src/server/{forward-proxy.js → network/forward-proxy.js} +10 -18
  355. package/src/server/{middlewares.js → network/middlewares.js} +3 -3
  356. package/src/server/network/node-capability.js +98 -0
  357. package/src/server/{peer.js → network/peer.js} +3 -3
  358. package/src/server/{proxy.js → network/proxy.js} +7 -13
  359. package/src/server/network/router.js +356 -0
  360. package/src/server/{tls.js → network/tls.js} +2 -2
  361. package/src/server/{underpost-compression.js → network/underpost-compression.js} +1 -1
  362. package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +7 -7
  363. package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +2 -2
  364. package/src/server/{cri.js → ops/cri.js} +2 -2
  365. package/src/server/{cron.js → ops/cron.js} +349 -74
  366. package/src/server/ops/event-notification.js +284 -0
  367. package/src/server/{logger.js → ops/logger.js} +108 -23
  368. package/src/server/ops/monitoring.js +1724 -0
  369. package/src/server/{systemd.js → ops/systemd.js} +55 -1
  370. package/src/server/{conf.js → runtime/conf.js} +513 -422
  371. package/src/server/runtime/config-scope.js +215 -0
  372. package/src/server/runtime/environment.js +149 -0
  373. package/src/server/{process.js → runtime/process.js} +153 -12
  374. package/src/server/{runtime-status.js → runtime/runtime-status.js} +155 -16
  375. package/src/server/{runtime.js → runtime/runtime.js} +12 -8
  376. package/src/server/{start.js → runtime/start.js} +128 -39
  377. package/src/server/{auth.js → security/auth.js} +7 -8
  378. package/src/server/security/container-storage.js +382 -0
  379. package/src/server/{crypto.js → security/crypto.js} +1 -1
  380. package/src/server/{selinux.js → security/selinux.js} +1 -1
  381. package/src/server/{backup.js → storage/backup.js} +17 -8
  382. package/src/server/{data-query.js → storage/data-query.js} +1 -1
  383. package/src/server/{downloader.js → storage/downloader.js} +2 -2
  384. package/src/server/storage/repository.js +68 -0
  385. package/src/server.js +4 -7
  386. package/src/ws/IoInterface.js +1 -1
  387. package/src/ws/IoServer.js +1 -1
  388. package/src/ws/core/core.ws.connection.js +1 -1
  389. package/src/ws/core/core.ws.emit.js +1 -1
  390. package/src/ws/core/core.ws.server.js +1 -1
  391. package/src/ws/default/default.ws.connection.js +1 -1
  392. package/src/ws/default/default.ws.emit.js +1 -1
  393. package/src/ws/default/default.ws.server.js +1 -1
  394. package/test/e2e/event-e2e-public-ingress-down.js +45 -0
  395. package/test/e2e/event-e2e-wireguard-server-down.js +38 -0
  396. package/test/e2e/event-e2e-wireguard-spoke-down.js +40 -0
  397. package/test/{api.test.js → integration/app/api.test.js} +33 -14
  398. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +30 -0
  399. package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +1 -1
  400. package/test/integration/app/cyberia/cyberia-load.test.js +467 -0
  401. package/test/integration/app/cyberia/fallback-world-capture.test.js +253 -0
  402. package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -0
  403. package/test/{shape-generator.test.js → integration/app/cyberia/shape-generator.test.js} +1 -1
  404. package/test/integration/infra/1-security/config-scope.test.js +305 -0
  405. package/test/integration/infra/1-security/container-storage.test.js +356 -0
  406. package/test/integration/infra/1-security/in-pod-cli-surface.test.js +86 -0
  407. package/test/integration/infra/1-security/secret-onboarding.test.js +930 -0
  408. package/test/{selinux.test.js → integration/infra/1-security/selinux.test.js} +2 -4
  409. package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js} +415 -61
  410. package/test/integration/infra/1-security/systemd-service.test.js +70 -0
  411. package/test/integration/infra/1-security/underpost-config-secret.test.js +367 -0
  412. package/test/integration/infra/2-network/dns-firewall.test.js +566 -0
  413. package/test/integration/infra/2-network/node-capability.test.js +182 -0
  414. package/test/integration/infra/2-network/wireguard-cli.test.js +1163 -0
  415. package/test/{wireguard-edge.test.js → integration/infra/2-network/wireguard-edge.test.js} +609 -55
  416. package/test/{cluster-instances.test.js → integration/infra/3-cluster/cluster-instances.test.js} +11 -15
  417. package/test/{deploy-node-placement.test.js → integration/infra/3-cluster/deploy-node-placement.test.js} +3 -3
  418. package/test/integration/infra/3-cluster/docker-compose-stack.test.js +313 -0
  419. package/test/integration/infra/4-ingress/deploy-routes.test.js +379 -0
  420. package/test/integration/infra/4-ingress/gateway-static-assets.test.js +453 -0
  421. package/test/{instance-traffic-plan.test.js → integration/infra/4-ingress/instance-traffic-plan.test.js} +10 -6
  422. package/test/{underpost-gateway.test.js → integration/infra/4-ingress/underpost-gateway.test.js} +6 -6
  423. package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +4 -4
  424. package/test/integration/infra/5-observability/cron-jobs.test.js +652 -0
  425. package/test/{deploy-monitor.test.js → integration/infra/5-observability/deploy-monitor.test.js} +29 -31
  426. package/test/integration/infra/5-observability/event-notification.test.js +197 -0
  427. package/test/integration/infra/5-observability/event-remediation.test.js +502 -0
  428. package/test/integration/infra/5-observability/event-targets.test.js +1174 -0
  429. package/test/integration/infra/5-observability/monitoring-stack.test.js +902 -0
  430. package/test/support/shell-harness.js +73 -0
  431. package/test/unit/build-template.test.js +97 -0
  432. package/test/unit/build-workflow-offline.test.js +132 -0
  433. package/test/unit/catalog.test.js +71 -0
  434. package/test/unit/client-build-docs.test.js +334 -0
  435. package/test/unit/conf-loading.test.js +303 -0
  436. package/test/unit/conf-resolution.test.js +979 -0
  437. package/test/unit/coverage-artifact.test.js +130 -0
  438. package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
  439. package/test/unit/deploy-env-resolution.test.js +38 -0
  440. package/test/unit/deploy-log-table.test.js +447 -0
  441. package/test/unit/execution-profiles.test.js +219 -0
  442. package/test/unit/fleet-sync-source.test.js +149 -0
  443. package/test/unit/logger-redaction.test.js +112 -0
  444. package/test/unit/package.test.js +403 -0
  445. package/test/unit/prepare-host.test.js +299 -0
  446. package/test/unit/process-environment.test.js +18 -0
  447. package/test/unit/propagation-message.test.js +78 -0
  448. package/test/unit/redeploy-plan.test.js +24 -0
  449. package/test/unit/release-bump.test.js +474 -0
  450. package/test/unit/ssh-output-redaction.test.js +28 -0
  451. package/test/unit/start-options.test.js +46 -0
  452. package/test/unit/test-tiers.test.js +193 -0
  453. package/vitest.config.js +58 -0
  454. package/.nycrc +0 -9
  455. package/deploy/lib/logging.sh +0 -96
  456. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
  457. package/manifests/mongodb/pv-pvc.yaml +0 -59
  458. package/scripts/coverall-test-cyberia.sh +0 -24
  459. package/scripts/coverall-test.sh +0 -24
  460. package/scripts/link-local-underpost-cli.sh +0 -6
  461. package/src/api/user/user.build.js +0 -16
  462. package/src/cli/env.js +0 -177
  463. package/src/client/components/core/Blockchain.js +0 -41
  464. package/src/client/components/core/Blog.js +0 -9
  465. package/src/client/components/core/KeyboardAvoidance.js +0 -145
  466. package/src/client/public/default/assets/background/dark.jpg +0 -0
  467. package/src/client/public/default/assets/background/white0-min.jpg +0 -0
  468. package/src/client/public/default/assets/background/white0.jpg +0 -0
  469. package/src/client/public/doc/favicon.ico +0 -0
  470. package/src/client/public/doc/sitemap +0 -148
  471. 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 "$@"