colloq 0.2.0__py3-none-any.whl

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 (224) hide show
  1. colloq/__init__.py +12 -0
  2. colloq/__main__.py +315 -0
  3. colloq/_app/.colloq-dist.json +66 -0
  4. colloq/_app/LICENSE +21 -0
  5. colloq/_app/THIRD_PARTY_NOTICES.md +106 -0
  6. colloq/_app/cli/colloq.mjs +2517 -0
  7. colloq/_app/cli/launch.mjs +1572 -0
  8. colloq/_app/cli/package.json +5 -0
  9. colloq/_app/cli/public-url-lease.mjs +198 -0
  10. colloq/_app/kernel/Dockerfile +93 -0
  11. colloq/_app/kernel/environments/base-gpu.txt +21 -0
  12. colloq/_app/kernel/environments/base.txt +12 -0
  13. colloq/_app/kernel/environments/cv.txt +15 -0
  14. colloq/_app/kernel/environments/gpu.txt +17 -0
  15. colloq/_app/kernel/environments/kaggle-base.txt +20 -0
  16. colloq/_app/kernel/environments/pvz-geo.txt +6 -0
  17. colloq/_app/kernel/requirements.txt +18 -0
  18. colloq/_app/package.json +20 -0
  19. colloq/_app/scripts/backup-local.sh +165 -0
  20. colloq/_app/scripts/backup.sh +34 -0
  21. colloq/_app/scripts/cluster.sh +492 -0
  22. colloq/_app/scripts/dns.sh +251 -0
  23. colloq/_app/scripts/host.sh +1514 -0
  24. colloq/_app/scripts/lib.sh +62 -0
  25. colloq/_app/scripts/restore.sh +338 -0
  26. colloq/_app/scripts/runtime-backup.py +373 -0
  27. colloq/_app/scripts/state-lock.py +85 -0
  28. colloq/_app/server/assets/fonts/HSESans-Black.otf +0 -0
  29. colloq/_app/server/assets/fonts/HSESans-Bold.otf +0 -0
  30. colloq/_app/server/assets/fonts/HSESans-Regular.otf +0 -0
  31. colloq/_app/server/assets/fonts/JetBrainsMono-Medium.ttf +0 -0
  32. colloq/_app/server/assets/fonts/JetBrainsMono-OFL.txt +93 -0
  33. colloq/_app/server/dist/local-cleanup.js +15987 -0
  34. colloq/_app/server/dist/runtime-smoke.js +17026 -0
  35. colloq/_app/server/dist/server.js +54333 -0
  36. colloq/_app/server/package.json +6 -0
  37. colloq/_app/web/dist/assets/AdminScreen-C-cg0GlJ.css +1 -0
  38. colloq/_app/web/dist/assets/AdminScreen-C-cg0GlJ.css.br +0 -0
  39. colloq/_app/web/dist/assets/AdminScreen-C-cg0GlJ.css.gz +0 -0
  40. colloq/_app/web/dist/assets/AdminScreen-fu4cB6Ra.js +105 -0
  41. colloq/_app/web/dist/assets/AdminScreen-fu4cB6Ra.js.br +0 -0
  42. colloq/_app/web/dist/assets/AdminScreen-fu4cB6Ra.js.gz +0 -0
  43. colloq/_app/web/dist/assets/App-CMXNqOjp.css +1 -0
  44. colloq/_app/web/dist/assets/App-CMXNqOjp.css.br +0 -0
  45. colloq/_app/web/dist/assets/App-CMXNqOjp.css.gz +0 -0
  46. colloq/_app/web/dist/assets/App-DvCPzIRO.js +26 -0
  47. colloq/_app/web/dist/assets/App-DvCPzIRO.js.br +0 -0
  48. colloq/_app/web/dist/assets/App-DvCPzIRO.js.gz +0 -0
  49. colloq/_app/web/dist/assets/CellOutputs-Dl-AZL45.js +42 -0
  50. colloq/_app/web/dist/assets/CellOutputs-Dl-AZL45.js.br +0 -0
  51. colloq/_app/web/dist/assets/CellOutputs-Dl-AZL45.js.gz +0 -0
  52. colloq/_app/web/dist/assets/CommandPalette-DhBGho7_.js +3 -0
  53. colloq/_app/web/dist/assets/CommandPalette-DhBGho7_.js.br +0 -0
  54. colloq/_app/web/dist/assets/CommandPalette-DhBGho7_.js.gz +0 -0
  55. colloq/_app/web/dist/assets/CompetitionsScreen-Vu3RpHmf.js +3 -0
  56. colloq/_app/web/dist/assets/CompetitionsScreen-Vu3RpHmf.js.br +0 -0
  57. colloq/_app/web/dist/assets/CompetitionsScreen-Vu3RpHmf.js.gz +0 -0
  58. colloq/_app/web/dist/assets/ConsoleView-B2uyYvFh.js +1 -0
  59. colloq/_app/web/dist/assets/ConsoleView-B2uyYvFh.js.br +0 -0
  60. colloq/_app/web/dist/assets/ConsoleView-B2uyYvFh.js.gz +0 -0
  61. colloq/_app/web/dist/assets/ConsoleView-DLbp0Jul.css +1 -0
  62. colloq/_app/web/dist/assets/DependencyBundleCard-B392XMVZ.js +4 -0
  63. colloq/_app/web/dist/assets/DependencyBundleCard-B392XMVZ.js.br +0 -0
  64. colloq/_app/web/dist/assets/DependencyBundleCard-B392XMVZ.js.gz +0 -0
  65. colloq/_app/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  66. colloq/_app/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  67. colloq/_app/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  68. colloq/_app/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  69. colloq/_app/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  70. colloq/_app/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  71. colloq/_app/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  72. colloq/_app/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  73. colloq/_app/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  74. colloq/_app/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  75. colloq/_app/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  76. colloq/_app/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  77. colloq/_app/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  78. colloq/_app/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  79. colloq/_app/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  80. colloq/_app/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  81. colloq/_app/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  82. colloq/_app/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  83. colloq/_app/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  84. colloq/_app/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  85. colloq/_app/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  86. colloq/_app/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  87. colloq/_app/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  88. colloq/_app/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  89. colloq/_app/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  90. colloq/_app/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  91. colloq/_app/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  92. colloq/_app/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  93. colloq/_app/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  94. colloq/_app/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  95. colloq/_app/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  96. colloq/_app/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  97. colloq/_app/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  98. colloq/_app/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  99. colloq/_app/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  100. colloq/_app/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  101. colloq/_app/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  102. colloq/_app/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  103. colloq/_app/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  104. colloq/_app/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  105. colloq/_app/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  106. colloq/_app/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  107. colloq/_app/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  108. colloq/_app/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  109. colloq/_app/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  110. colloq/_app/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  111. colloq/_app/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  112. colloq/_app/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  113. colloq/_app/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  114. colloq/_app/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  115. colloq/_app/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  116. colloq/_app/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  117. colloq/_app/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  118. colloq/_app/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  119. colloq/_app/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  120. colloq/_app/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  121. colloq/_app/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  122. colloq/_app/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  123. colloq/_app/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  124. colloq/_app/web/dist/assets/PultWindow-B3cc8537.css +1 -0
  125. colloq/_app/web/dist/assets/PultWindow-B3cc8537.css.br +0 -0
  126. colloq/_app/web/dist/assets/PultWindow-B3cc8537.css.gz +0 -0
  127. colloq/_app/web/dist/assets/PultWindow-DPOUhSqt.js +8 -0
  128. colloq/_app/web/dist/assets/PultWindow-DPOUhSqt.js.br +0 -0
  129. colloq/_app/web/dist/assets/PultWindow-DPOUhSqt.js.gz +0 -0
  130. colloq/_app/web/dist/assets/ReaderScreen-CV_0VAvW.js +9 -0
  131. colloq/_app/web/dist/assets/ReaderScreen-CV_0VAvW.js.br +0 -0
  132. colloq/_app/web/dist/assets/ReaderScreen-CV_0VAvW.js.gz +0 -0
  133. colloq/_app/web/dist/assets/SessionScreen-BrcB6CLH.js +132 -0
  134. colloq/_app/web/dist/assets/SessionScreen-BrcB6CLH.js.br +0 -0
  135. colloq/_app/web/dist/assets/SessionScreen-BrcB6CLH.js.gz +0 -0
  136. colloq/_app/web/dist/assets/SessionScreen-Dek9dK3C.css +1 -0
  137. colloq/_app/web/dist/assets/SessionScreen-Dek9dK3C.css.br +0 -0
  138. colloq/_app/web/dist/assets/SessionScreen-Dek9dK3C.css.gz +0 -0
  139. colloq/_app/web/dist/assets/actions-DLPyolgo.js +1 -0
  140. colloq/_app/web/dist/assets/admin-en-BHmR7Co0.js +9 -0
  141. colloq/_app/web/dist/assets/admin-en-BHmR7Co0.js.br +0 -0
  142. colloq/_app/web/dist/assets/admin-en-BHmR7Co0.js.gz +0 -0
  143. colloq/_app/web/dist/assets/admin-ru-Dr6Z36Zi.js +11 -0
  144. colloq/_app/web/dist/assets/admin-ru-Dr6Z36Zi.js.br +0 -0
  145. colloq/_app/web/dist/assets/admin-ru-Dr6Z36Zi.js.gz +0 -0
  146. colloq/_app/web/dist/assets/codemirror-6LUh0q1l.js +32 -0
  147. colloq/_app/web/dist/assets/codemirror-6LUh0q1l.js.br +0 -0
  148. colloq/_app/web/dist/assets/codemirror-6LUh0q1l.js.gz +0 -0
  149. colloq/_app/web/dist/assets/competitions-en-zw57uAGL.js +5 -0
  150. colloq/_app/web/dist/assets/competitions-en-zw57uAGL.js.br +0 -0
  151. colloq/_app/web/dist/assets/competitions-en-zw57uAGL.js.gz +0 -0
  152. colloq/_app/web/dist/assets/competitions-ru-BBQUjAx0.js +5 -0
  153. colloq/_app/web/dist/assets/competitions-ru-BBQUjAx0.js.br +0 -0
  154. colloq/_app/web/dist/assets/competitions-ru-BBQUjAx0.js.gz +0 -0
  155. colloq/_app/web/dist/assets/editor-theme-CVYtpZDy.js +1 -0
  156. colloq/_app/web/dist/assets/editor-theme-CVYtpZDy.js.br +0 -0
  157. colloq/_app/web/dist/assets/editor-theme-CVYtpZDy.js.gz +0 -0
  158. colloq/_app/web/dist/assets/index-CwbYK31L.css +1 -0
  159. colloq/_app/web/dist/assets/index-CwbYK31L.css.br +0 -0
  160. colloq/_app/web/dist/assets/index-CwbYK31L.css.gz +0 -0
  161. colloq/_app/web/dist/assets/index-Di16xdmz.js +2 -0
  162. colloq/_app/web/dist/assets/index-Di16xdmz.js.br +0 -0
  163. colloq/_app/web/dist/assets/index-Di16xdmz.js.gz +0 -0
  164. colloq/_app/web/dist/assets/katex-CS_ezkZp.css +1 -0
  165. colloq/_app/web/dist/assets/katex-CS_ezkZp.css.br +0 -0
  166. colloq/_app/web/dist/assets/katex-CS_ezkZp.css.gz +0 -0
  167. colloq/_app/web/dist/assets/katex-DLorvT8M.js +257 -0
  168. colloq/_app/web/dist/assets/katex-DLorvT8M.js.br +0 -0
  169. colloq/_app/web/dist/assets/katex-DLorvT8M.js.gz +0 -0
  170. colloq/_app/web/dist/assets/pdf-DEp6sk-T.js +55 -0
  171. colloq/_app/web/dist/assets/pdf-DEp6sk-T.js.br +0 -0
  172. colloq/_app/web/dist/assets/pdf-DEp6sk-T.js.gz +0 -0
  173. colloq/_app/web/dist/assets/persistence.svelte-vcuCBcT5.js +1 -0
  174. colloq/_app/web/dist/assets/persistence.svelte-vcuCBcT5.js.br +0 -0
  175. colloq/_app/web/dist/assets/persistence.svelte-vcuCBcT5.js.gz +0 -0
  176. colloq/_app/web/dist/assets/reader-en-UCGE3U6V.js +3 -0
  177. colloq/_app/web/dist/assets/reader-en-UCGE3U6V.js.br +0 -0
  178. colloq/_app/web/dist/assets/reader-en-UCGE3U6V.js.gz +0 -0
  179. colloq/_app/web/dist/assets/reader-ru-CaiNj0K8.js +3 -0
  180. colloq/_app/web/dist/assets/reader-ru-CaiNj0K8.js.br +0 -0
  181. colloq/_app/web/dist/assets/reader-ru-CaiNj0K8.js.gz +0 -0
  182. colloq/_app/web/dist/assets/render-CjP3RuRT.js +155 -0
  183. colloq/_app/web/dist/assets/render-CjP3RuRT.js.br +0 -0
  184. colloq/_app/web/dist/assets/render-CjP3RuRT.js.gz +0 -0
  185. colloq/_app/web/dist/assets/render.svelte-B4_hAJmX.js +0 -0
  186. colloq/_app/web/dist/assets/render.svelte-B4_hAJmX.js.br +0 -0
  187. colloq/_app/web/dist/assets/render.svelte-B4_hAJmX.js.gz +0 -0
  188. colloq/_app/web/dist/assets/room-en-Bh8L6NcO.js +3 -0
  189. colloq/_app/web/dist/assets/room-en-Bh8L6NcO.js.br +0 -0
  190. colloq/_app/web/dist/assets/room-en-Bh8L6NcO.js.gz +0 -0
  191. colloq/_app/web/dist/assets/room-ru-Bar2GgS0.js +3 -0
  192. colloq/_app/web/dist/assets/room-ru-Bar2GgS0.js.br +0 -0
  193. colloq/_app/web/dist/assets/room-ru-Bar2GgS0.js.gz +0 -0
  194. colloq/_app/web/dist/assets/seminar-link-CRaZl9Ku.js +1 -0
  195. colloq/_app/web/dist/assets/seminar-link-CRaZl9Ku.js.br +0 -0
  196. colloq/_app/web/dist/assets/seminar-link-CRaZl9Ku.js.gz +0 -0
  197. colloq/_app/web/dist/assets/seminar-link-DoTugxxA.css +1 -0
  198. colloq/_app/web/dist/assets/seminar-link-DoTugxxA.css.br +0 -0
  199. colloq/_app/web/dist/assets/seminar-link-DoTugxxA.css.gz +0 -0
  200. colloq/_app/web/dist/assets/yjs-rjB3hdnL.js +6 -0
  201. colloq/_app/web/dist/assets/yjs-rjB3hdnL.js.br +0 -0
  202. colloq/_app/web/dist/assets/yjs-rjB3hdnL.js.gz +0 -0
  203. colloq/_app/web/dist/fonts/JetBrainsMono-OFL.txt +93 -0
  204. colloq/_app/web/dist/fonts/hse-sans-400.woff2 +0 -0
  205. colloq/_app/web/dist/fonts/hse-sans-600.woff2 +0 -0
  206. colloq/_app/web/dist/fonts/hse-sans-700.woff2 +0 -0
  207. colloq/_app/web/dist/fonts/hse-sans-900.woff2 +0 -0
  208. colloq/_app/web/dist/fonts/jetbrains-mono-cyrillic-ext.woff2 +0 -0
  209. colloq/_app/web/dist/fonts/jetbrains-mono-cyrillic.woff2 +0 -0
  210. colloq/_app/web/dist/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
  211. colloq/_app/web/dist/fonts/jetbrains-mono-latin.woff2 +0 -0
  212. colloq/_app/web/dist/index.html +268 -0
  213. colloq/_app/web/dist/og/colloq.png +0 -0
  214. colloq/_app/web/dist/pdf/pdf.worker.min.mjs +29 -0
  215. colloq/_app/web/dist/pdf/pdf.worker.min.mjs.br +0 -0
  216. colloq/_app/web/dist/pdf/pdf.worker.min.mjs.gz +0 -0
  217. colloq/_app/web/dist/plotly/plotly.min.js +3620 -0
  218. colloq/_app/web/dist/plotly/plotly.min.js.br +0 -0
  219. colloq/_app/web/dist/plotly/plotly.min.js.gz +0 -0
  220. colloq/_version.py +5 -0
  221. colloq-0.2.0.dist-info/METADATA +124 -0
  222. colloq-0.2.0.dist-info/RECORD +224 -0
  223. colloq-0.2.0.dist-info/WHEEL +4 -0
  224. colloq-0.2.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env bash
2
+ # One-node production deployment. No release is inferred from this working tree.
3
+ set -euo pipefail
4
+ ORIGINAL_ARGS=("$@")
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ STATE="${COLLOQ_STATE_DIR:-/var/lib/colloq}"
7
+ NS=colloq
8
+ PORT=30080
9
+ CMD="${1:-help}"; [ "$#" -eq 0 ] || shift
10
+ RELEASE="${RELEASE_MANIFEST:-}"
11
+ ENV_FILE=""
12
+ REGISTRY_CONFIG=""
13
+ PUBLIC_URL=""
14
+ EXPECT_URL=""
15
+ if [ "$CMD" = public-url ]; then PUBLIC_URL="${1:-}"; [ "$#" -eq 0 ] || shift; fi
16
+ while [ "$#" -gt 0 ]; do
17
+ case "$1" in
18
+ --release) RELEASE="${2:?--release requires a file}"; shift 2 ;;
19
+ --if-current) EXPECT_URL="${2:?--if-current requires a URL}"; shift 2 ;;
20
+ --env-file) ENV_FILE="${2:?--env-file requires a file}"; shift 2 ;;
21
+ --registry-config) REGISTRY_CONFIG="${2:?--registry-config requires docker config JSON}"; shift 2 ;;
22
+ --*) printf 'unknown argument: %s\n' "$1" >&2; exit 1 ;;
23
+ *) [ -z "$RELEASE" ] || { echo 'only one release file is allowed' >&2; exit 1; }; RELEASE="$1"; shift ;;
24
+ esac
25
+ done
26
+ die() { printf 'cluster: %s\n' "$*" >&2; exit 1; }
27
+ release() { python3 "$SCRIPT_DIR/release.py" "$@"; }
28
+ kube() (
29
+ # Use the matching multicall client directly. Some packaged k3s builds pass
30
+ # the literal subcommand through to kubectl when invoked as `k3s kubectl`.
31
+ # Never inherit an operator's unrelated cluster context.
32
+ export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
33
+ exec -a kubectl "$(command -v k3s)" "$@"
34
+ )
35
+ k() { kube -n "$NS" "$@"; }
36
+ need_root() {
37
+ [ "$(uname -s)" = Linux ] || die 'production deployment requires a Linux VM'
38
+ [ "$(id -u)" = 0 ] || die 'run this host provisioning command as root'
39
+ }
40
+ need_cluster() { command -v k3s >/dev/null || die 'k3s is not installed; install an explicit release first'; }
41
+ guard_restore() {
42
+ case "$CMD" in
43
+ prepare|install|update|rollback|start|public-url|smoke|gpu-preflight)
44
+ if [ -e "$STATE/.restore-in-progress" ] || [ -L "$STATE/.restore-in-progress" ]; then
45
+ die 'an incomplete restore is in progress; recover the verified backup before starting or changing workloads'
46
+ fi ;;
47
+ esac
48
+ }
49
+ data_release() {
50
+ if [ -f "$STATE/recovery/release.json" ]; then printf '%s\n' "$STATE/recovery/release.json";
51
+ else printf '%s\n' "$STATE/releases/current.json"; fi
52
+ }
53
+ # Файл оператора, по которому рендерится брокер: новый --env-file или
54
+ # config.env прошлой установки — тот же выбор, что делает prepare. Из него
55
+ # брокер получает RUNTIME_KERNEL_MEMORY[_MAX]; без этого память комнат по
56
+ # умолчанию задавалась только правкой Deployment, и update её стирал.
57
+ operator_env() {
58
+ if [ -n "$ENV_FILE" ]; then printf '%s\n' "$ENV_FILE"
59
+ elif [ -r "$STATE/config.env" ]; then printf '%s\n' "$STATE/config.env"; fi
60
+ }
61
+ # Проверить эти числа, пока ничего не остановлено: иначе плохое значение
62
+ # всплыло бы после бэкапа и остановки комнат — или CrashLoop-ом брокера.
63
+ check_operator_env() {
64
+ local file
65
+ file="$(operator_env)"
66
+ [ -z "$file" ] || release render --release "$1" --state-dir "$STATE" --env-file "$file" >/dev/null
67
+ }
68
+
69
+ case "$CMD" in
70
+ validate|render|prepare|install|update|rollback)
71
+ [ -n "$RELEASE" ] || die 'an explicit --release FILE is required'
72
+ release validate --release "$RELEASE" >/dev/null
73
+ ;;
74
+ esac
75
+ case "$CMD" in
76
+ validate) exec python3 "$SCRIPT_DIR/release.py" validate --release "$RELEASE" ;;
77
+ render)
78
+ runtime=(); env_file="$(operator_env)"; [ -z "$env_file" ] || runtime=(--env-file "$env_file")
79
+ exec python3 "$SCRIPT_DIR/release.py" render --release "$RELEASE" --state-dir "$STATE" --node-name "${COLLOQ_NODE_NAME:-colloq}" ${runtime[@]+"${runtime[@]}"} ;;
80
+ help)
81
+ cat <<'HELP'
82
+ cluster.sh validate|render --release FILE inspect before host changes
83
+ cluster.sh prepare --release FILE [--env-file FILE] [--registry-config FILE]
84
+ install resources, all writers stopped
85
+ cluster.sh install|update|rollback --release FILE [--env-file FILE]
86
+ prepare, start, verify readiness
87
+ (env file: app settings, RUNTIME_KERNEL_MEMORY[_MAX])
88
+ cluster.sh start|stop|status|logs manage the installed application
89
+ cluster.sh public-url https://example.edu update public URL, restart app if running
90
+ cluster.sh public-url URL --if-current OLD only while it is still OLD; exit 4 otherwise
91
+ cluster.sh smoke|gpu-preflight disposable room/CUDA execution checks
92
+
93
+ State: /var/lib/colloq (COLLOQ_STATE_DIR overrides). App: 127.0.0.1:30080.
94
+ Install/prepare require an explicit release with immutable image digests.
95
+ Updates require a verified backup first. k3s upgrades are separate operations.
96
+ HELP
97
+ exit 0 ;;
98
+ esac
99
+ guard_restore
100
+ case "$CMD" in
101
+ prepare|install|update|rollback)
102
+ release verify-tooling --release "$RELEASE" --tooling-root "$SCRIPT_DIR/.." >/dev/null ;;
103
+ esac
104
+ need_root
105
+ umask 077
106
+ case "$CMD" in
107
+ prepare|install|update|rollback|start|stop|public-url|smoke|gpu-preflight|restore-services)
108
+ if ! python3 "$SCRIPT_DIR/state-lock.py" held --state "$STATE"; then
109
+ exec python3 "$SCRIPT_DIR/state-lock.py" run --state "$STATE" -- bash "$SCRIPT_DIR/cluster.sh" "${ORIGINAL_ARGS[@]}"
110
+ fi
111
+ # A restore may have begun after the early read-only check but before the lock.
112
+ guard_restore
113
+ ;;
114
+ esac
115
+
116
+ stop_writers() {
117
+ k scale deployment/colloq-app deployment/colloq-runtime --replicas=0
118
+ k wait --for=delete pod -l 'colloq.dev/role in (app,runtime)' --timeout=90s
119
+ # Stop the broker before deleting kernels: it must not recreate a room.
120
+ k delete pods -l colloq.kind=room-kernel --wait=true --timeout=120s
121
+ k delete services -l 'app.kubernetes.io/managed-by=colloq-runtime,colloq.dev/retired!=true' --ignore-not-found
122
+ }
123
+
124
+ firewall() {
125
+ command -v iptables >/dev/null || die 'iptables is required to keep Kubernetes host ports private'
126
+ # This does not block the NetworkPolicy local-node exception. See deploy/k3s/README.md.
127
+ iptables -N COLLOQ-HOST 2>/dev/null || true
128
+ iptables -F COLLOQ-HOST
129
+ iptables -A COLLOQ-HOST -i lo -j RETURN
130
+ iptables -A COLLOQ-HOST -s 10.42.0.0/16 -j RETURN
131
+ iptables -A COLLOQ-HOST -p tcp -m multiport --dports 6443,10250,30080 -j DROP
132
+ iptables -A COLLOQ-HOST -p udp --dport 8472 -j DROP
133
+ iptables -A COLLOQ-HOST -j RETURN
134
+ iptables -C INPUT -j COLLOQ-HOST 2>/dev/null || iptables -I INPUT 1 -j COLLOQ-HOST
135
+ if [ -s /proc/net/if_inet6 ]; then
136
+ command -v ip6tables >/dev/null || die 'ip6tables is required on IPv6-enabled hosts'
137
+ ip6tables -N COLLOQ-HOST 2>/dev/null || true
138
+ ip6tables -F COLLOQ-HOST
139
+ ip6tables -A COLLOQ-HOST -i lo -j RETURN
140
+ ip6tables -A COLLOQ-HOST -p tcp -m multiport --dports 6443,10250,30080 -j DROP
141
+ ip6tables -A COLLOQ-HOST -p udp --dport 8472 -j DROP
142
+ ip6tables -A COLLOQ-HOST -j RETURN
143
+ ip6tables -C INPUT -j COLLOQ-HOST 2>/dev/null || ip6tables -I INPUT 1 -j COLLOQ-HOST
144
+ fi
145
+ }
146
+
147
+ bootstrap() {
148
+ local wanted have tmp
149
+ wanted="$(release field --release "$RELEASE" --field k3sVersion)"
150
+ if systemctl cat colloq.service >/dev/null 2>&1; then
151
+ die 'legacy colloq.service exists; stop/remove that root service and migrate its data explicitly before installing k3s workloads'
152
+ fi
153
+ gpu_toolkit
154
+ if command -v k3s >/dev/null; then
155
+ have="$(k3s --version | head -1 | awk '{print $3}')"
156
+ [ "$have" = "$wanted" ] || die "installed k3s $have differs from release $wanted; upgrade k3s separately after backing up its datastore and token"
157
+ [ -f /etc/rancher/k3s/colloq-managed ] || die 'existing k3s is not Colloq-managed; review deploy/k3s/README.md before migration'
158
+ if [ "${GPU_RUNTIME_CHANGED:-0}" = 1 ]; then systemctl restart k3s; fi
159
+ systemctl is-active --quiet k3s || systemctl start k3s
160
+ return
161
+ fi
162
+ [ "$(uname -m)" = x86_64 ] || die 'this release installer currently supports Linux amd64 only'
163
+ command -v systemctl >/dev/null || die 'systemd is required'
164
+ command -v iptables >/dev/null || die 'install iptables before bootstrapping k3s'
165
+ command -v curl >/dev/null || die 'curl is required'
166
+ tmp="$(mktemp -d)"
167
+ # Both artifacts come from the explicit upstream release, never a latest channel.
168
+ curl -fL --retry 3 "https://github.com/k3s-io/k3s/releases/download/$wanted/k3s" -o "$tmp/k3s"
169
+ curl -fL --retry 3 "https://github.com/k3s-io/k3s/releases/download/$wanted/sha256sum-amd64.txt" -o "$tmp/checksums"
170
+ (cd "$tmp" && awk '$2 == "k3s" || $2 == "*k3s" {print}' checksums > selected && test -s selected && sha256sum -c selected)
171
+ install -m 0755 "$tmp/k3s" /usr/local/bin/k3s
172
+ mkdir -p /etc/rancher/k3s
173
+ cat > /etc/rancher/k3s/config.yaml <<'CONFIG'
174
+ disable:
175
+ - traefik
176
+ - servicelb
177
+ - local-storage
178
+ write-kubeconfig-mode: "0600"
179
+ secrets-encryption: true
180
+ cluster-cidr: 10.42.0.0/16
181
+ service-cidr: 10.43.0.0/16
182
+ kube-proxy-arg:
183
+ - nodeport-addresses=127.0.0.0/8
184
+ kubelet-arg:
185
+ - pod-max-pids=256
186
+ CONFIG
187
+ cat > /etc/systemd/system/k3s.service <<'UNIT'
188
+ [Unit]
189
+ Description=Colloq single-node k3s
190
+ Wants=network-online.target
191
+ After=network-online.target colloq-firewall.service
192
+ Requires=colloq-firewall.service
193
+ [Service]
194
+ Type=notify
195
+ ExecStart=/usr/local/bin/k3s server
196
+ Delegate=yes
197
+ KillMode=process
198
+ Restart=always
199
+ RestartSec=5
200
+ LimitNOFILE=1048576
201
+ LimitNPROC=infinity
202
+ TasksMax=infinity
203
+ [Install]
204
+ WantedBy=multi-user.target
205
+ UNIT
206
+ # Persist host exposure restrictions across reboot without a broad firewall reset.
207
+ install -m 0755 "$SCRIPT_DIR/cluster.sh" /usr/local/sbin/colloq-cluster-firewall
208
+ cat > /etc/systemd/system/colloq-firewall.service <<'UNIT'
209
+ [Unit]
210
+ Description=Keep Colloq Kubernetes host ports private
211
+ Before=k3s.service
212
+ [Service]
213
+ Type=oneshot
214
+ ExecStart=/usr/local/sbin/colloq-cluster-firewall firewall
215
+ RemainAfterExit=yes
216
+ [Install]
217
+ WantedBy=multi-user.target
218
+ UNIT
219
+ touch /etc/rancher/k3s/colloq-managed
220
+ systemctl daemon-reload
221
+ systemctl enable --now colloq-firewall.service
222
+ systemctl enable --now k3s
223
+ }
224
+
225
+ gpu_toolkit() {
226
+ [ "$(release field --release "$RELEASE" --field gpu)" = true ] || return 0
227
+ command -v nvidia-smi >/dev/null || die 'GPU release requires a preinstalled working NVIDIA host driver; the installer will not replace it'
228
+ nvidia-smi --query-gpu=index,name --format=csv,noheader || die 'host NVIDIA driver is not working'
229
+ local wanted have package matches=1
230
+ wanted="$(release field --release "$RELEASE" --field toolkitVersion)"
231
+ command -v apt-get >/dev/null || die 'pinned GPU toolkit installation supports Debian/Ubuntu apt hosts'
232
+ for package in nvidia-container-toolkit nvidia-container-toolkit-base libnvidia-container-tools libnvidia-container1; do
233
+ have="$(dpkg-query -W -f='${Version}' "$package" 2>/dev/null || true)"
234
+ if [ -n "$have" ] && dpkg --compare-versions "$have" gt "$wanted"; then
235
+ die "$package $have is newer than release pin $wanted; select a release with compatible tooling or explicitly downgrade the toolkit before retrying"
236
+ fi
237
+ if [ "$have" != "$wanted" ]; then matches=0; fi
238
+ done
239
+ if [ "$matches" = 0 ]; then
240
+ command -v gpg >/dev/null || die 'install gnupg before configuring the signed NVIDIA package repository'
241
+ mkdir -p /usr/share/keyrings
242
+ curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | gpg --dearmor --yes -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
243
+ curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
244
+ | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#' \
245
+ > /etc/apt/sources.list.d/nvidia-container-toolkit.list
246
+ apt-get update -qq
247
+ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
248
+ "nvidia-container-toolkit=$wanted" "nvidia-container-toolkit-base=$wanted" \
249
+ "libnvidia-container-tools=$wanted" "libnvidia-container1=$wanted"
250
+ GPU_RUNTIME_CHANGED=1
251
+ fi
252
+ command -v nvidia-container-runtime >/dev/null || die 'NVIDIA container runtime is missing after pinned toolkit installation'
253
+ # k3s detects nvidia-container-runtime and generates its own containerd config.
254
+ # Do not configure Docker or edit k3s's generated containerd TOML.
255
+ }
256
+
257
+ gpu_plugin() {
258
+ [ "$(release field --release "$1" --field gpu)" = true ] || return 0
259
+ # The plugin runs in a trusted namespace; carry only the pull credential there.
260
+ k get secret colloq-registry -o json | python3 -c 'import json,sys; value=json.load(sys.stdin); value["metadata"]={"name":"colloq-registry","namespace":"kube-system"}; print(json.dumps(value))' \
261
+ | kube apply -f - >/dev/null
262
+ release gpu --release "$1" | kube apply -f -
263
+ kube -n kube-system rollout status daemonset/colloq-nvidia-device-plugin --timeout=180s
264
+ # A running plugin can precede the kubelet's next node-status update.
265
+ local count deadline=$((SECONDS + 60))
266
+ while [ "$SECONDS" -lt "$deadline" ]; do
267
+ count="$(k get nodes --request-timeout=5s -o 'jsonpath={.items[0].status.allocatable.nvidia\.com/gpu}' || true)"
268
+ if [[ "$count" =~ ^[1-9][0-9]*$ ]]; then return 0; fi
269
+ sleep 2
270
+ done
271
+ die 'NVIDIA plugin has no allocatable GPU after waiting for registration; check driver, toolkit and k3s runtime logs'
272
+ }
273
+
274
+ gpu_preflight() {
275
+ local manifest="$STATE/releases/current.json"
276
+ [ "$(release field --release "$manifest" --field gpu)" = true ] || return 0
277
+ nvidia-smi --query-gpu=index,name --format=csv,noheader || die 'NVIDIA host driver failed'
278
+ # Never evict a room to obtain a GPU. A busy device makes the bounded probe fail.
279
+ k delete pod colloq-gpu-check --ignore-not-found --wait=true >/dev/null
280
+ release gpu-smoke --release "$manifest" | k create -f - >/dev/null
281
+ if ! k wait --for=jsonpath='{.status.phase}'=Succeeded pod/colloq-gpu-check --timeout=200s; then
282
+ k describe pod colloq-gpu-check >&2
283
+ k logs colloq-gpu-check >&2 || true
284
+ k delete pod colloq-gpu-check --wait=true >/dev/null
285
+ die 'CUDA probe failed or no exclusive GPU was free; existing rooms were not evicted'
286
+ fi
287
+ k logs colloq-gpu-check
288
+ k delete pod colloq-gpu-check --wait=true >/dev/null
289
+ }
290
+
291
+ wait_for_node() {
292
+ local nodes state deadline=$((SECONDS + 180))
293
+ while [ "$SECONDS" -lt "$deadline" ]; do
294
+ # The API can be reachable before the kubelet creates its first Node.
295
+ if nodes="$(k get nodes --request-timeout=5s -o json 2>/dev/null)"; then
296
+ state="$(printf '%s' "$nodes" | python3 -c '
297
+ import json,sys
298
+ nodes=json.load(sys.stdin)["items"]
299
+ if len(nodes)>1:
300
+ print("multiple")
301
+ elif len(nodes)==1:
302
+ node=nodes[0]
303
+ hostname=node.get("metadata",{}).get("labels",{}).get("kubernetes.io/hostname")
304
+ ready=any(c.get("type")=="Ready" and c.get("status")=="True" for c in node.get("status",{}).get("conditions",[]))
305
+ if ready and hostname:
306
+ print("ready " + hostname)
307
+ ')" || die 'cannot parse Kubernetes node status'
308
+ case "$state" in
309
+ multiple) die 'this deployment supports exactly one node' ;;
310
+ ready\ *) printf '%s\n' "${state#ready }"; return 0 ;;
311
+ esac
312
+ fi
313
+ sleep 2
314
+ done
315
+ die 'no single ready Kubernetes node registered within 180 seconds; check k3s and kubelet logs'
316
+ }
317
+
318
+ prepare() {
319
+ local effective node current policy_version data extras=() runtime=()
320
+ guard_restore
321
+ mkdir -p "$STATE/releases" "$STATE/secrets" "$STATE/data" "$STATE/workspace"
322
+ chmod 0750 "$STATE/data" "$STATE/workspace"
323
+ chown 1000:1000 "$STATE/data" "$STATE/workspace"
324
+ current="$STATE/releases/current.json"
325
+ data="$(data_release)"
326
+ if [ -f "$current" ]; then extras=(--previous "$current"); fi
327
+ if [ -f "$data" ]; then extras+=(--data-release "$data"); fi
328
+ if [ "$CMD" = rollback ]; then extras+=(--rollback); fi
329
+ effective="$(mktemp "$STATE/releases/.release.XXXXXX")"
330
+ release merge --release "$RELEASE" "${extras[@]}" > "$effective"
331
+ if [ -f "$STATE/recovery/release.json" ]; then
332
+ release merge --release "$effective" --previous "$STATE/recovery/release.json" --data-release "$data" > "$effective.recovery"
333
+ mv "$effective.recovery" "$effective"
334
+ fi
335
+ check_operator_env "$effective"
336
+ RELEASE="$effective" bootstrap
337
+ need_cluster
338
+ node="$(wait_for_node)"
339
+ k create namespace "$NS" --dry-run=client -o json | k apply -f - >/dev/null
340
+ policy_version="$(release field --release "$effective" --field policyVersion)"
341
+ k label namespace "$NS" pod-security.kubernetes.io/enforce=restricted \
342
+ "pod-security.kubernetes.io/enforce-version=$policy_version" \
343
+ pod-security.kubernetes.io/audit=restricted "pod-security.kubernetes.io/audit-version=$policy_version" \
344
+ pod-security.kubernetes.io/warn=restricted "pod-security.kubernetes.io/warn-version=$policy_version" --overwrite >/dev/null
345
+ if [ -n "$REGISTRY_CONFIG" ]; then
346
+ k create secret generic colloq-registry --type=kubernetes.io/dockerconfigjson --from-file=".dockerconfigjson=$REGISTRY_CONFIG" --dry-run=client -o json | k apply -f - >/dev/null
347
+ elif ! k get secret colloq-registry >/dev/null 2>&1; then
348
+ k create secret generic colloq-registry --type=kubernetes.io/dockerconfigjson --from-literal='.dockerconfigjson={"auths":{}}' >/dev/null
349
+ fi
350
+ gpu_plugin "$effective"
351
+ # Kubernetes uses the same imagePullSecret as the actual workloads. ctr/crictl
352
+ # pulls would bypass it and break otherwise valid private-registry releases.
353
+ k delete pod colloq-image-check --ignore-not-found --wait=true >/dev/null
354
+ release prepull --release "$effective" | k create -f - >/dev/null
355
+ if ! k wait --for=jsonpath='{.status.phase}'=Succeeded pod/colloq-image-check --timeout=900s; then
356
+ k describe pod colloq-image-check >&2
357
+ die 'release images could not be pulled/run as UID 1000; inspect the preflight Pod before retrying (updates leave writers stopped after backup)'
358
+ fi
359
+ k delete pod colloq-image-check --wait=true >/dev/null
360
+ if k get deployment colloq-app >/dev/null 2>&1; then stop_writers; fi
361
+ for secret in runtime-token room-secret; do
362
+ if [ ! -s "$STATE/secrets/$secret" ]; then
363
+ python3 -c 'import secrets; print(secrets.token_hex(32))' > "$STATE/secrets/$secret"
364
+ fi
365
+ chmod 0600 "$STATE/secrets/$secret"
366
+ done
367
+ k create secret generic colloq-runtime-auth --from-file="runtime-token=$STATE/secrets/runtime-token" --dry-run=client -o json | k apply -f - >/dev/null
368
+ k create secret generic colloq-room-secret --from-file="room-secret=$STATE/secrets/room-secret" --dry-run=client -o json | k apply -f - >/dev/null
369
+ if [ -n "$ENV_FILE" ]; then cp "$ENV_FILE" "$STATE/config.env"; chmod 0600 "$STATE/config.env"; fi
370
+ if [ -f "$STATE/config.env" ]; then
371
+ release config --release "$effective" --env-file "$STATE/config.env" | k apply -f - >/dev/null
372
+ elif ! k get secret colloq-app-config >/dev/null 2>&1; then
373
+ release config --release "$effective" | k apply -f - >/dev/null
374
+ fi
375
+ [ ! -f "$STATE/config.env" ] || runtime=(--env-file "$STATE/config.env")
376
+ release render --release "$effective" --node-name "$node" --state-dir "$STATE" ${runtime[@]+"${runtime[@]}"} | k apply -f -
377
+ [ ! -f "$current" ] || cp "$current" "$STATE/releases/previous.json"
378
+ mv "$effective" "$current"
379
+ printf 'Release prepared; all writers stopped. Restore if needed, then cluster.sh start.\n'
380
+ }
381
+
382
+ wait_for_app() {
383
+ local timeout deadline=$((SECONDS + 60))
384
+ # Pod readiness can precede kube-proxy programming the localhost NodePort.
385
+ while [ "$SECONDS" -lt "$deadline" ]; do
386
+ timeout=$((deadline - SECONDS))
387
+ [ "$timeout" -gt 0 ] || break
388
+ if [ "$timeout" -gt 5 ]; then timeout=5; fi
389
+ if curl -fsS --connect-timeout 2 --max-time "$timeout" "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1; then
390
+ return 0
391
+ fi
392
+ if [ "$SECONDS" -lt "$deadline" ]; then sleep 1; fi
393
+ done
394
+ die "app health endpoint http://127.0.0.1:$PORT/api/health did not become reachable within 60 seconds; check app logs, Service endpoints and kube-proxy"
395
+ }
396
+
397
+ start() {
398
+ guard_restore
399
+ need_cluster
400
+ if [ -f "$STATE/recovery/release.json" ]; then
401
+ release validate --release "$STATE/releases/current.json" --data-release "$STATE/recovery/release.json" >/dev/null
402
+ fi
403
+ k scale deployment/colloq-runtime --replicas=1
404
+ k rollout status deployment/colloq-runtime --timeout=180s
405
+ # Once the app is admitted it may migrate SQLite even if readiness later fails.
406
+ # Consume the restored-schema override before that first possible writer.
407
+ release consume-recovery --release "$STATE/releases/current.json" --state-dir "$STATE"
408
+ k scale deployment/colloq-app --replicas=1
409
+ k rollout status deployment/colloq-app --timeout=180s
410
+ wait_for_app
411
+ gpu_preflight
412
+ printf 'Colloq ready on http://127.0.0.1:%s; use cluster.sh smoke to verify two-room execution.\n' "$PORT"
413
+ }
414
+
415
+ case "$CMD" in
416
+ firewall) firewall ;;
417
+ prepare) prepare ;;
418
+ install) prepare; start ;;
419
+ update|rollback)
420
+ [ -f "$STATE/releases/current.json" ] || die 'no installed release; use install'
421
+ # Validate schema compatibility before taking writers down for a backup.
422
+ data="$(data_release)"
423
+ release merge --release "$RELEASE" --previous "$STATE/releases/current.json" --data-release "$data" >/dev/null
424
+ check_operator_env "$RELEASE"
425
+ [ -x "$SCRIPT_DIR/backup.sh" ] || die 'backup.sh is required before an update'
426
+ MODE=consistent RESUME=0 RELEASE="$data" COLLOQ_STATE_DIR="$STATE" "$SCRIPT_DIR/backup.sh"
427
+ prepare; start ;;
428
+ start) start ;;
429
+ stop) need_cluster; stop_writers ;;
430
+ restore-services)
431
+ need_cluster
432
+ STATE="$STATE" python3 - <<'PY'
433
+ import json, os, pathlib
434
+ try:
435
+ marker = json.loads((pathlib.Path(os.environ['STATE']) / '.restore-in-progress').read_text())
436
+ if marker.get('phase') != 'files-restored':
437
+ raise ValueError('invalid phase')
438
+ except (OSError, ValueError, AttributeError):
439
+ raise SystemExit('restore-services requires the verified files-restored recovery phase')
440
+ PY
441
+ writers="$(k get pods -l 'colloq.dev/role in (app,runtime,kernel)' -o name)"
442
+ [ -z "$writers" ] \
443
+ || die 'restore-services refuses while app, broker or kernel Pods exist'
444
+ writers="$(k get pods -l colloq.kind=room-kernel -o name)"
445
+ [ -z "$writers" ] \
446
+ || die 'restore-services refuses while room Pods exist'
447
+ k delete services -l app.kubernetes.io/managed-by=colloq-runtime --ignore-not-found --wait=true --timeout=90s
448
+ ;;
449
+ status) need_cluster; k get deployments,pods,pvc; curl -fsS --max-time 5 "http://127.0.0.1:$PORT/api/health" ;;
450
+ logs) need_cluster; k logs -f deployment/colloq-app --tail=80 ;;
451
+ gpu-preflight) need_cluster; gpu_preflight ;;
452
+ smoke) need_cluster; k exec deployment/colloq-app -- node /app/dist/runtime-smoke.js ;;
453
+ public-url)
454
+ need_cluster
455
+ changed=0
456
+ PUBLIC_URL="$PUBLIC_URL" EXPECT_URL="$EXPECT_URL" STATE="$STATE" python3 - <<'PY' || changed=$?
457
+ import os, pathlib, urllib.parse
458
+ url = os.environ['PUBLIC_URL']
459
+ parsed = urllib.parse.urlsplit(url)
460
+ if parsed.scheme not in ('http', 'https') or not parsed.netloc or '\n' in url or '\r' in url:
461
+ raise SystemExit('public-url requires an absolute HTTP(S) URL')
462
+ file = pathlib.Path(os.environ['STATE']) / 'config.env'
463
+ lines = file.read_text().splitlines() if file.exists() else []
464
+ current = next((x[11:] for x in reversed(lines) if x.startswith('PUBLIC_URL=')), None)
465
+ if current == url:
466
+ raise SystemExit(3)
467
+ # --if-current: снять адрес может только тот, кто его ставил. scripts/host.sh
468
+ # на выходе возвращает localhost, и без этой сверки его уборка затирала адрес,
469
+ # который за это время поставил кто-то другой (второй туннель, оператор руками),
470
+ # — живая ссылка молча превращалась в localhost. Сверка стоит здесь, под замком
471
+ # состояния, рядом с записью: сверить снаружи и записать потом — это гонка.
472
+ expected = os.environ.get('EXPECT_URL') or ''
473
+ if expected and current != expected:
474
+ raise SystemExit(4)
475
+ temp = file.with_suffix('.tmp')
476
+ temp.write_text('\n'.join([x for x in lines if not x.startswith('PUBLIC_URL=')] + ['PUBLIC_URL=' + url, '']))
477
+ temp.replace(file)
478
+ PY
479
+ [ "$changed" != 3 ] || exit 0
480
+ if [ "$changed" = 4 ]; then
481
+ printf 'cluster: public URL is no longer %s; left unchanged\n' "$EXPECT_URL" >&2
482
+ exit 4
483
+ fi
484
+ [ "$changed" = 0 ] || die 'could not update public URL'
485
+ release config --release "$STATE/releases/current.json" --env-file "$STATE/config.env" | k apply -f - >/dev/null
486
+ if [ "$(k get deployment colloq-app -o jsonpath='{.spec.replicas}')" != 0 ]; then
487
+ k rollout restart deployment/colloq-app
488
+ k rollout status deployment/colloq-app --timeout=180s
489
+ fi
490
+ ;;
491
+ *) die "unknown command: $CMD" ;;
492
+ esac