@geekmidas/cli 10.0.0-alpha.98 → 10.0.0-alpha.99

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 (187) hide show
  1. package/dist/{GoDaddyProvider-C2fK-zeQ.mjs → GoDaddyProvider-206NguW1.mjs} +2 -2
  2. package/dist/{GoDaddyProvider-C2fK-zeQ.mjs.map → GoDaddyProvider-206NguW1.mjs.map} +1 -1
  3. package/dist/{GoDaddyProvider-SLlyDwD1.cjs → GoDaddyProvider-D1aQy2i1.cjs} +2 -2
  4. package/dist/{GoDaddyProvider-SLlyDwD1.cjs.map → GoDaddyProvider-D1aQy2i1.cjs.map} +1 -1
  5. package/dist/{LegacyStateStore-CAvxM8U5.cjs → LegacyStateStore-CgJEr1G7.cjs} +3 -3
  6. package/dist/{LegacyStateStore-CAvxM8U5.cjs.map → LegacyStateStore-CgJEr1G7.cjs.map} +1 -1
  7. package/dist/{LegacyStateStore-D9QDWCst.mjs → LegacyStateStore-wmFfdzuh.mjs} +3 -3
  8. package/dist/{LegacyStateStore-D9QDWCst.mjs.map → LegacyStateStore-wmFfdzuh.mjs.map} +1 -1
  9. package/dist/{LocalStateStore-QcrdjCl6.cjs → LocalStateStore-BD14kra9.cjs} +2 -2
  10. package/dist/{LocalStateStore-QcrdjCl6.cjs.map → LocalStateStore-BD14kra9.cjs.map} +1 -1
  11. package/dist/{LocalStateStore-DPQSqkSb.mjs → LocalStateStore-C5mNp91D.mjs} +2 -2
  12. package/dist/{LocalStateStore-DPQSqkSb.mjs.map → LocalStateStore-C5mNp91D.mjs.map} +1 -1
  13. package/dist/{S3StateStore-jorgKaee.mjs → S3StateStore-CEGbsDHi.mjs} +4 -4
  14. package/dist/{S3StateStore-jorgKaee.mjs.map → S3StateStore-CEGbsDHi.mjs.map} +1 -1
  15. package/dist/{S3StateStore-C16tVikU.cjs → S3StateStore-Csu_kJCy.cjs} +5 -5
  16. package/dist/{S3StateStore-C16tVikU.cjs.map → S3StateStore-Csu_kJCy.cjs.map} +1 -1
  17. package/dist/{SSMStateStore-Dvjt_INw.mjs → SSMStateStore-Bd62qwp4.mjs} +2 -2
  18. package/dist/{SSMStateStore-Dvjt_INw.mjs.map → SSMStateStore-Bd62qwp4.mjs.map} +1 -1
  19. package/dist/{SSMStateStore-mPAEr2P2.cjs → SSMStateStore-Rp21jtP5.cjs} +2 -2
  20. package/dist/{SSMStateStore-mPAEr2P2.cjs.map → SSMStateStore-Rp21jtP5.cjs.map} +1 -1
  21. package/dist/{add-CZIIsTcv.mjs → add-DyIEYI97.mjs} +4 -4
  22. package/dist/{add-CZIIsTcv.mjs.map → add-DyIEYI97.mjs.map} +1 -1
  23. package/dist/{add-SRk65Udu.cjs → add-Iuo3ao5E.cjs} +5 -5
  24. package/dist/{add-SRk65Udu.cjs.map → add-Iuo3ao5E.cjs.map} +1 -1
  25. package/dist/{bundler-B0Pj5FXT.cjs → bundler-Bgoq8IyR.cjs} +5 -5
  26. package/dist/{bundler-B0Pj5FXT.cjs.map → bundler-Bgoq8IyR.cjs.map} +1 -1
  27. package/dist/{bundler-DxJ-cIEt.mjs → bundler-Q6Kp8b0r.mjs} +5 -5
  28. package/dist/{bundler-DxJ-cIEt.mjs.map → bundler-Q6Kp8b0r.mjs.map} +1 -1
  29. package/dist/{cli-CV85bY3-.mjs → cli-B9BudUV0.mjs} +2 -4
  30. package/dist/cli-B9BudUV0.mjs.map +1 -0
  31. package/dist/{cli-CRDJWWmZ.cjs → cli-W2DMRIbP.cjs} +2 -4
  32. package/dist/cli-W2DMRIbP.cjs.map +1 -0
  33. package/dist/compose-CIrFNWHb.cjs +3147 -0
  34. package/dist/compose-CIrFNWHb.cjs.map +1 -0
  35. package/dist/compose-CLsRcHkA.mjs +3052 -0
  36. package/dist/compose-CLsRcHkA.mjs.map +1 -0
  37. package/dist/{config-pBzLfNef.d.mts → config-BVdHfj8b.d.cts} +4 -4
  38. package/dist/{config-CS76CzXM.d.cts.map → config-BVdHfj8b.d.cts.map} +1 -1
  39. package/dist/{config-CS76CzXM.d.cts → config-D2OqCmAM.d.mts} +4 -4
  40. package/dist/{config-pBzLfNef.d.mts.map → config-D2OqCmAM.d.mts.map} +1 -1
  41. package/dist/config.cjs +1 -1
  42. package/dist/config.d.cts +3 -3
  43. package/dist/config.d.mts +3 -3
  44. package/dist/config.mjs +1 -1
  45. package/dist/deploy/public.cjs +9 -8
  46. package/dist/deploy/public.cjs.map +1 -1
  47. package/dist/deploy/public.d.cts +5 -16
  48. package/dist/deploy/public.d.cts.map +1 -1
  49. package/dist/deploy/public.d.mts +5 -16
  50. package/dist/deploy/public.d.mts.map +1 -1
  51. package/dist/deploy/public.mjs +8 -7
  52. package/dist/deploy/public.mjs.map +1 -1
  53. package/dist/{deploy-277jek46.cjs → deploy-CbJMJsgX.cjs} +26 -62
  54. package/dist/deploy-CbJMJsgX.cjs.map +1 -0
  55. package/dist/{deploy-Dw8g3sLY.mjs → deploy-n_IB8r6H.mjs} +17 -59
  56. package/dist/deploy-n_IB8r6H.mjs.map +1 -0
  57. package/dist/{discover-DJCCgimv.d.mts → discover-Caf6Hnv1.d.cts} +2 -2
  58. package/dist/{discover-CU1LjCOf.d.cts.map → discover-Caf6Hnv1.d.cts.map} +1 -1
  59. package/dist/{discover-CU1LjCOf.d.cts → discover-Cd5M2jPv.d.mts} +2 -2
  60. package/dist/{discover-DJCCgimv.d.mts.map → discover-Cd5M2jPv.d.mts.map} +1 -1
  61. package/dist/{dns-LqWzPb8l.mjs → dns-C_LPS3qV.mjs} +3 -3
  62. package/dist/{dns-LqWzPb8l.mjs.map → dns-C_LPS3qV.mjs.map} +1 -1
  63. package/dist/{dns-Dhjbnjgl.cjs → dns-kp8To8Qj.cjs} +3 -3
  64. package/dist/{dns-Dhjbnjgl.cjs.map → dns-kp8To8Qj.cjs.map} +1 -1
  65. package/dist/{dokploy-D_uCbZQ7.mjs → dokploy-Dl_kNVeE.mjs} +2 -2
  66. package/dist/{dokploy-D_uCbZQ7.mjs.map → dokploy-Dl_kNVeE.mjs.map} +1 -1
  67. package/dist/{dokploy-Dw4oCXqR.cjs → dokploy-gRIae9Ht.cjs} +2 -2
  68. package/dist/{dokploy-Dw4oCXqR.cjs.map → dokploy-gRIae9Ht.cjs.map} +1 -1
  69. package/dist/{encryption-C0uKYE9v.mjs → encryption-C3DcVZQs.mjs} +3 -3
  70. package/dist/{encryption-C0uKYE9v.mjs.map → encryption-C3DcVZQs.mjs.map} +1 -1
  71. package/dist/{encryption-CtVPHWWq.cjs → encryption-CmQvQOSe.cjs} +3 -3
  72. package/dist/{encryption-CtVPHWWq.cjs.map → encryption-CmQvQOSe.cjs.map} +1 -1
  73. package/dist/{engine-BE5PdE8S.mjs → engine-DBXuhHyf.mjs} +5 -4
  74. package/dist/{engine-BE5PdE8S.mjs.map → engine-DBXuhHyf.mjs.map} +1 -1
  75. package/dist/{engine-HI8aLw1M.cjs → engine-Dk19Mxc4.cjs} +27 -26
  76. package/dist/{engine-HI8aLw1M.cjs.map → engine-Dk19Mxc4.cjs.map} +1 -1
  77. package/dist/{github-BCdfZ9zO.cjs → github-Ds2RYnSU.cjs} +9 -8
  78. package/dist/{github-BCdfZ9zO.cjs.map → github-Ds2RYnSU.cjs.map} +1 -1
  79. package/dist/{github-CT3X720_.mjs → github-RawRLERq.mjs} +7 -6
  80. package/dist/{github-CT3X720_.mjs.map → github-RawRLERq.mjs.map} +1 -1
  81. package/dist/identity-CBXr4wNi.mjs +129 -0
  82. package/dist/identity-CBXr4wNi.mjs.map +1 -0
  83. package/dist/identity-CbLfgQNT.cjs +182 -0
  84. package/dist/identity-CbLfgQNT.cjs.map +1 -0
  85. package/dist/{index-DKtB01YC.d.cts → index-BU4G20G6.d.cts} +2 -2
  86. package/dist/{index-DKtB01YC.d.cts.map → index-BU4G20G6.d.cts.map} +1 -1
  87. package/dist/{index-ZAUrXH6Q.d.mts → index-yDbd1WcM.d.mts} +2 -2
  88. package/dist/{index-ZAUrXH6Q.d.mts.map → index-yDbd1WcM.d.mts.map} +1 -1
  89. package/dist/index.cjs +131 -141
  90. package/dist/index.cjs.map +1 -1
  91. package/dist/index.mjs +128 -138
  92. package/dist/index.mjs.map +1 -1
  93. package/dist/{migrate-CCZUgxMK.mjs → migrate-CZIGT3kB.mjs} +3 -3
  94. package/dist/{migrate-CCZUgxMK.mjs.map → migrate-CZIGT3kB.mjs.map} +1 -1
  95. package/dist/{migrate-DBGyoBvZ.cjs → migrate-CxZYzF_u.cjs} +3 -3
  96. package/dist/{migrate-DBGyoBvZ.cjs.map → migrate-CxZYzF_u.cjs.map} +1 -1
  97. package/dist/{migrate-DH-lOK3j.mjs → migrate-DR1x96I4.mjs} +3 -3
  98. package/dist/{migrate-DH-lOK3j.mjs.map → migrate-DR1x96I4.mjs.map} +1 -1
  99. package/dist/{migrate-Cs7kQxD5.cjs → migrate-DjrTwWJA.cjs} +3 -3
  100. package/dist/{migrate-Cs7kQxD5.cjs.map → migrate-DjrTwWJA.cjs.map} +1 -1
  101. package/dist/openapi.cjs +1 -1
  102. package/dist/openapi.d.cts +1 -1
  103. package/dist/openapi.d.mts +1 -1
  104. package/dist/openapi.mjs +1 -1
  105. package/dist/{projectBucket-DrkEht-j.mjs → projectBucket-BYOxRnHY.mjs} +2 -2
  106. package/dist/{projectBucket-DrkEht-j.mjs.map → projectBucket-BYOxRnHY.mjs.map} +1 -1
  107. package/dist/{projectBucket-BjeDcAYy.cjs → projectBucket-CDu5rXjp.cjs} +2 -2
  108. package/dist/{projectBucket-BjeDcAYy.cjs.map → projectBucket-CDu5rXjp.cjs.map} +1 -1
  109. package/dist/reconcile/public.d.cts +2 -2
  110. package/dist/reconcile/public.d.mts +2 -2
  111. package/dist/{resolve-OfLaxaxY.mjs → resolve-CNYbGH8g.mjs} +3 -3
  112. package/dist/{resolve-OfLaxaxY.mjs.map → resolve-CNYbGH8g.mjs.map} +1 -1
  113. package/dist/{resolve-CScrqgdC.cjs → resolve-DC98Rl1t.cjs} +3 -3
  114. package/dist/{resolve-CScrqgdC.cjs.map → resolve-DC98Rl1t.cjs.map} +1 -1
  115. package/dist/{rollback-DQBSN-i7.mjs → rollback-BkIkE8Qe.mjs} +5 -5
  116. package/dist/{rollback-DQBSN-i7.mjs.map → rollback-BkIkE8Qe.mjs.map} +1 -1
  117. package/dist/{rollback-D7p4wJNO.cjs → rollback-DBfLvC8k.cjs} +5 -5
  118. package/dist/{rollback-D7p4wJNO.cjs.map → rollback-DBfLvC8k.cjs.map} +1 -1
  119. package/dist/{runtime-BlHOVx8L.d.cts → runtime-CLz3bf07.d.mts} +2 -2
  120. package/dist/{runtime-BlHOVx8L.d.cts.map → runtime-CLz3bf07.d.mts.map} +1 -1
  121. package/dist/{runtime-DMq9C3KB.d.mts → runtime-CUWf0IUk.d.cts} +2 -2
  122. package/dist/{runtime-DMq9C3KB.d.mts.map → runtime-CUWf0IUk.d.cts.map} +1 -1
  123. package/dist/{s3-CUKzBBae.mjs → s3-BwhQk3QM.mjs} +3 -3
  124. package/dist/{s3-CUKzBBae.mjs.map → s3-BwhQk3QM.mjs.map} +1 -1
  125. package/dist/{s3-QQi4sNOQ.cjs → s3-DLp0V-Ox.cjs} +3 -3
  126. package/dist/{s3-QQi4sNOQ.cjs.map → s3-DLp0V-Ox.cjs.map} +1 -1
  127. package/dist/{sandbox-Cm4X6HRh.d.mts → sandbox-CfpLWTPP.d.mts} +2 -2
  128. package/dist/{sandbox-ArJ_hEKk.d.cts.map → sandbox-CfpLWTPP.d.mts.map} +1 -1
  129. package/dist/{sandbox-ArJ_hEKk.d.cts → sandbox-D1XeNEJ9.d.cts} +2 -2
  130. package/dist/{sandbox-Cm4X6HRh.d.mts.map → sandbox-D1XeNEJ9.d.cts.map} +1 -1
  131. package/dist/{seeds-Df4BI-T3.cjs → seeds-BNHSpjNP.cjs} +9 -179
  132. package/dist/seeds-BNHSpjNP.cjs.map +1 -0
  133. package/dist/{seeds-BvxWUGKJ.mjs → seeds-C4bZ8L-T.mjs} +10 -132
  134. package/dist/seeds-C4bZ8L-T.mjs.map +1 -0
  135. package/dist/{sniffer-BqjXvcaK.mjs → sniffer-BIBm9dAb.mjs} +7 -7
  136. package/dist/{sniffer-BqjXvcaK.mjs.map → sniffer-BIBm9dAb.mjs.map} +1 -1
  137. package/dist/{sniffer-DZeQJKty.cjs → sniffer-Cylp9W2D.cjs} +7 -7
  138. package/dist/{sniffer-DZeQJKty.cjs.map → sniffer-Cylp9W2D.cjs.map} +1 -1
  139. package/dist/{stages-6SEoRA4x.mjs → stages-CcS9vnNu.mjs} +3 -17
  140. package/dist/stages-CcS9vnNu.mjs.map +1 -0
  141. package/dist/{stages-DH5z89eT.cjs → stages-DdBV7msu.cjs} +3 -17
  142. package/dist/stages-DdBV7msu.cjs.map +1 -0
  143. package/dist/target/public.cjs +1 -1
  144. package/dist/target/public.d.cts +2 -2
  145. package/dist/target/public.d.mts +2 -2
  146. package/dist/target/public.mjs +1 -1
  147. package/dist/{types-Drpl9HX9.d.mts → types-BhThob0H.d.mts} +29 -13
  148. package/dist/types-BhThob0H.d.mts.map +1 -0
  149. package/dist/{types-DipUP9zp.d.cts → types-DwoLHelT.d.cts} +29 -13
  150. package/dist/types-DwoLHelT.d.cts.map +1 -0
  151. package/dist/workspace/index.cjs +1 -1
  152. package/dist/workspace/index.d.cts +2 -2
  153. package/dist/workspace/index.d.mts +2 -2
  154. package/dist/workspace/index.mjs +1 -1
  155. package/dist/{workspace-BBeT6Pil.mjs → workspace-D5CpOp1y.mjs} +22 -2
  156. package/dist/workspace-D5CpOp1y.mjs.map +1 -0
  157. package/dist/{workspace-BJ78Bgvm.cjs → workspace-DRjdu1OY.cjs} +22 -2
  158. package/dist/workspace-DRjdu1OY.cjs.map +1 -0
  159. package/package.json +15 -15
  160. package/dist/cli-CRDJWWmZ.cjs.map +0 -1
  161. package/dist/cli-CV85bY3-.mjs.map +0 -1
  162. package/dist/compose-CQQRRJC3.mjs +0 -1511
  163. package/dist/compose-CQQRRJC3.mjs.map +0 -1
  164. package/dist/compose-DMY4ouEk.cjs +0 -1558
  165. package/dist/compose-DMY4ouEk.cjs.map +0 -1
  166. package/dist/deploy-277jek46.cjs.map +0 -1
  167. package/dist/deploy-Dw8g3sLY.mjs.map +0 -1
  168. package/dist/dns-4RAiVFa7.cjs +0 -55
  169. package/dist/dns-4RAiVFa7.cjs.map +0 -1
  170. package/dist/dns-CU22K_i0.mjs +0 -44
  171. package/dist/dns-CU22K_i0.mjs.map +0 -1
  172. package/dist/secrets-AA_tQUl-.mjs +0 -36
  173. package/dist/secrets-AA_tQUl-.mjs.map +0 -1
  174. package/dist/secrets-B_biKsKx.cjs +0 -47
  175. package/dist/secrets-B_biKsKx.cjs.map +0 -1
  176. package/dist/seeds-BvxWUGKJ.mjs.map +0 -1
  177. package/dist/seeds-Df4BI-T3.cjs.map +0 -1
  178. package/dist/stack-A6MAFZ6u.mjs +0 -1201
  179. package/dist/stack-A6MAFZ6u.mjs.map +0 -1
  180. package/dist/stack-BpkZEEfB.cjs +0 -1308
  181. package/dist/stack-BpkZEEfB.cjs.map +0 -1
  182. package/dist/stages-6SEoRA4x.mjs.map +0 -1
  183. package/dist/stages-DH5z89eT.cjs.map +0 -1
  184. package/dist/types-DipUP9zp.d.cts.map +0 -1
  185. package/dist/types-Drpl9HX9.d.mts.map +0 -1
  186. package/dist/workspace-BBeT6Pil.mjs.map +0 -1
  187. package/dist/workspace-BJ78Bgvm.cjs.map +0 -1
@@ -1,1558 +0,0 @@
1
- const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
2
- const require_errors = require("./errors-DMC4Umsb.cjs");
3
- const require_run = require("./run-fVZ-rLnc.cjs");
4
- const require_discover = require("./discover-DPgu-ScS.cjs");
5
- const require_workspace = require("./workspace-BJ78Bgvm.cjs");
6
- const require_define = require("./define-crmMcXFS.cjs");
7
- const require_seeds = require("./seeds-Df4BI-T3.cjs");
8
- const require_drivers = require("./drivers-DDojgzz0.cjs");
9
- const require_dns = require("./dns-Dhjbnjgl.cjs");
10
- const require_stack = require("./stack-BpkZEEfB.cjs");
11
- const require_secrets = require("./secrets-B_biKsKx.cjs");
12
- const require_databases = require("./databases-DGALFDG3.cjs");
13
- let node_path = require("node:path");
14
- let node_fs = require("node:fs");
15
- let node_fs_promises = require("node:fs/promises");
16
- let node_os = require("node:os");
17
- let node_child_process = require("node:child_process");
18
- let yaml = require("yaml");
19
- let node_https = require("node:https");
20
- //#region src/compose/docker.ts
21
- /**
22
- * The Docker operations `gkm compose` needs, behind an interface.
23
- *
24
- * Small on purpose, like reconcile's: the rules about *when* each runs — the
25
- * registry is asked before anything is pulled, nothing is started for a tag
26
- * that is missing — are asserted against a fake, and this file is only the
27
- * wiring to the real CLI.
28
- *
29
- * Every call is an argument array. Image refs, tags and paths come from the
30
- * workspace, and none of them reaches a shell.
31
- */
32
- /** `output` as `spawn` reads it. */
33
- const STDIO = {
34
- inherit: "inherit",
35
- stderr: [
36
- "ignore",
37
- 2,
38
- 2
39
- ],
40
- ignore: "ignore"
41
- };
42
- /** How a stack's docker commands run: where it is, where output goes. */
43
- function runOptions(stack, env) {
44
- return {
45
- cwd: stack.cwd,
46
- stdio: STDIO[stack.output ?? "inherit"],
47
- ...env ? { env } : {},
48
- ...stack.signal ? { signal: stack.signal } : {}
49
- };
50
- }
51
- /**
52
- * The host ports a `docker ps` Ports column publishes: `0.0.0.0:443->443/tcp,
53
- * [::]:443->443/tcp, 0.0.0.0:9000-9001->9000-9001/tcp`.
54
- */
55
- function publishedPorts(column) {
56
- const ports = /* @__PURE__ */ new Set();
57
- for (const match of column.matchAll(/(?:[\d.]+|\[[^\]]*\]):(\d+)(?:-(\d+))?->/g)) {
58
- const first = Number(match[1]);
59
- const last = match[2] ? Number(match[2]) : first;
60
- for (let port = first; port <= last; port++) ports.add(port);
61
- }
62
- return [...ports];
63
- }
64
- /** Each line `docker ps` printed, as the containers holding `port`. */
65
- function portHolders(output, port) {
66
- return output.split("\n").filter(Boolean).map((line) => line.split(" ")).filter(([, , , column = ""]) => publishedPorts(column).includes(port)).map(([container = "", project, service]) => ({
67
- container,
68
- ...project ? { project } : {},
69
- ...service ? { service } : {}
70
- }));
71
- }
72
- /** How long a stack has to pass its health checks. */
73
- const WAIT_TIMEOUT_S = 180;
74
- /** `docker compose port` printed nothing a port can be read from. */
75
- var ServicePortUnknown = class extends require_errors.GkmError {
76
- service;
77
- inside;
78
- output;
79
- constructor(service, inside, output) {
80
- super(`'${service}' publishes no host port for ${inside} (docker said: ${output.trim() || "nothing"}). Is it running? \`docker compose ps\` shows the stack.`);
81
- this.service = service;
82
- this.inside = inside;
83
- this.output = output;
84
- this.name = "ServicePortUnknown";
85
- }
86
- };
87
- /**
88
- * Whether `ref` names a registry on this machine's loopback — which the
89
- * daemon pulls from and pushes to over plain HTTP by default, and `docker
90
- * manifest inspect` reaches only when told `--insecure` (without it, a local
91
- * registry's image is reported as no such manifest).
92
- */
93
- function isLoopbackRegistry(ref) {
94
- const first = ref.split("/")[0];
95
- if (!ref.includes("/") || !/[.:]|^localhost$/.test(first)) return false;
96
- const host = first.replace(/:\d+$/, "");
97
- return host === "localhost" || /^127(?:\.\d{1,3}){3}$/.test(host);
98
- }
99
- /** `docker push` finished without saying what digest the registry stored. */
100
- var PushDigestUnknown = class extends require_errors.GkmError {
101
- ref;
102
- output;
103
- constructor(ref, output) {
104
- super(`Pushed ${ref}, but docker did not say which digest the registry stored (it said: ${output.trim().split("\n").slice(-3).join(" / ") || "nothing"}). Check the push with \`docker buildx imagetools inspect ${ref}\`.`);
105
- this.ref = ref;
106
- this.output = output;
107
- this.name = "PushDigestUnknown";
108
- }
109
- };
110
- /** The digest `docker push` reports last: `<tag>: digest: sha256:… size: …`. */
111
- function pushedDigest(output) {
112
- return [...output.matchAll(/digest: (sha256:[0-9a-f]{64})/g)].at(-1)?.[1];
113
- }
114
- function compose(stack, args) {
115
- return [
116
- "compose",
117
- "-p",
118
- stack.project,
119
- "-f",
120
- stack.file,
121
- ...(stack.overrides ?? []).flatMap((file) => ["-f", file]),
122
- ...args
123
- ];
124
- }
125
- /** Run a program and keep everything it said, whatever it exits with. */
126
- function capture(command, args, timeoutMs = 6e4) {
127
- return new Promise((resolve, reject) => {
128
- const child = (0, node_child_process.spawn)(command, [...args], {
129
- stdio: [
130
- "ignore",
131
- "pipe",
132
- "pipe"
133
- ],
134
- shell: false
135
- });
136
- let stdout = "";
137
- let stderr = "";
138
- child.stdout.on("data", (chunk) => {
139
- stdout += chunk.toString();
140
- });
141
- child.stderr.on("data", (chunk) => {
142
- stderr += chunk.toString();
143
- });
144
- const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
145
- timer.unref?.();
146
- child.on("error", (error) => {
147
- clearTimeout(timer);
148
- reject(error);
149
- });
150
- child.on("close", (code) => {
151
- clearTimeout(timer);
152
- resolve({
153
- code,
154
- stdout,
155
- stderr
156
- });
157
- });
158
- });
159
- }
160
- /**
161
- * Whether a failed `docker manifest inspect` means the tag is not there, as
162
- * opposed to the registry not answering.
163
- *
164
- * Read off the message because that is all the CLI gives. Anything not
165
- * recognisably "no such tag" is treated as the registry being unreachable:
166
- * a registry that refused credentials must not be reported as a tag that was
167
- * never pushed, or the fix offered would be the wrong one.
168
- */
169
- function isMissingManifest(output) {
170
- return /no such manifest|manifest unknown|manifest for .* not found|not found: manifest/i.test(output);
171
- }
172
- const dockerCompose = {
173
- async lookup(ref) {
174
- const { code, stdout, stderr } = await capture("docker", [
175
- "manifest",
176
- "inspect",
177
- ...isLoopbackRegistry(ref) ? ["--insecure"] : [],
178
- ref
179
- ]);
180
- if (code === 0) return {
181
- ref,
182
- status: "found"
183
- };
184
- const output = `${stderr}\n${stdout}`.trim();
185
- return isMissingManifest(output) ? {
186
- ref,
187
- status: "missing"
188
- } : {
189
- ref,
190
- status: "unreachable",
191
- detail: output
192
- };
193
- },
194
- async build(stack, services) {
195
- await require_run.run("docker", compose(stack, ["build", ...services]), runOptions(stack, {
196
- ...process.env,
197
- DOCKER_BUILDKIT: "1"
198
- }));
199
- },
200
- async pull(stack, services) {
201
- await require_run.run("docker", compose(stack, ["pull", ...services]), runOptions(stack));
202
- },
203
- async up(stack, services) {
204
- await require_run.run("docker", compose(stack, [
205
- "up",
206
- "-d",
207
- "--wait",
208
- `--wait-timeout=${WAIT_TIMEOUT_S}`,
209
- ...services ? [] : ["--remove-orphans"],
210
- ...services ?? []
211
- ]), runOptions(stack));
212
- },
213
- async down(stack) {
214
- await require_run.run("docker", compose(stack, ["down"]), runOptions(stack));
215
- },
216
- async port(stack, service, inside) {
217
- const output = await require_run.runOutput("docker", compose(stack, [
218
- "port",
219
- service,
220
- String(inside)
221
- ]), {
222
- cwd: stack.cwd,
223
- ...stack.signal ? { signal: stack.signal } : {}
224
- });
225
- const port = Number(output.trim().split(":").pop());
226
- if (!Number.isInteger(port) || port <= 0) throw new ServicePortUnknown(service, inside, output);
227
- return port;
228
- },
229
- async health(stack, service) {
230
- const { code, stdout } = await capture("docker", [...compose(stack, [
231
- "ps",
232
- "--format",
233
- "{{.Health}}",
234
- service
235
- ])]);
236
- if (code !== 0) return void 0;
237
- return stdout.trim().split("\n")[0] || void 0;
238
- },
239
- async copyOut(stack, service, from, to) {
240
- await (0, node_fs_promises.mkdir)((0, node_path.dirname)(to), { recursive: true });
241
- await require_run.run("docker", compose(stack, [
242
- "cp",
243
- `${service}:${from}`,
244
- to
245
- ]), runOptions(stack));
246
- },
247
- async push(stack, ref) {
248
- const output = await require_run.runOutput("docker", ["push", ref], {
249
- cwd: stack.cwd,
250
- ...stack.signal ? { signal: stack.signal } : {}
251
- });
252
- const digest = pushedDigest(output);
253
- if (!digest) throw new PushDigestUnknown(ref, output);
254
- return digest;
255
- },
256
- async digest(ref) {
257
- const { code, stdout } = await capture("docker", [
258
- "image",
259
- "inspect",
260
- "--format={{json .RepoDigests}} {{.Id}}",
261
- ref
262
- ]);
263
- if (code !== 0) return void 0;
264
- const [json = "[]", id] = stdout.trim().split(" ");
265
- const repository = ref.split("@")[0].replace(/:[\w][\w.-]*$/, "");
266
- try {
267
- const digests = JSON.parse(json);
268
- const match = Array.isArray(digests) ? digests.find((entry) => typeof entry === "string" && entry.startsWith(`${repository}@`)) : void 0;
269
- if (match) return match.slice(repository.length + 1);
270
- } catch {}
271
- return id || void 0;
272
- },
273
- async ensureNetwork(name) {
274
- const { code } = await capture("docker", [
275
- "network",
276
- "inspect",
277
- name
278
- ]);
279
- if (code === 0) return;
280
- const created = await capture("docker", [
281
- "network",
282
- "create",
283
- name
284
- ]);
285
- if (created.code !== 0 && !/already exists/.test(created.stderr)) throw new NetworkCreateFailed(name, created.stderr);
286
- },
287
- async publishers(port) {
288
- const { stdout } = await capture("docker", [
289
- "ps",
290
- "--format",
291
- "{{.Names}} {{.Label \"com.docker.compose.project\"}} {{.Label \"com.docker.compose.service\"}} {{.Ports}}"
292
- ]);
293
- return portHolders(stdout, port);
294
- }
295
- };
296
- /** `docker network create` failed. */
297
- var NetworkCreateFailed = class extends Error {
298
- network;
299
- output;
300
- constructor(network, output) {
301
- super(`Could not create the Docker network '${network}' (docker said: ${output.trim() || "nothing"}). Create it by hand with \`docker network create ${network}\` and run again.`);
302
- this.network = network;
303
- this.output = output;
304
- this.name = "NetworkCreateFailed";
305
- }
306
- };
307
- //#endregion
308
- //#region src/compose/edge.ts
309
- /**
310
- * The server's shared Traefik edge, on disk and in Docker: started when a
311
- * stack needs it, and each stack's routes written to — and removed from —
312
- * the directory it watches.
313
- *
314
- * The edge lives in the deploy user's gkm home (`$GKM_HOME/edge`, else
315
- * `~/.gkm/edge`): the user who runs `gkm compose` owns it, so nothing needs
316
- * root, and it sits beside the stage keys that same user already keeps.
317
- *
318
- * ~/.gkm/edge/
319
- * docker-compose.yml the edge's compose project (gkm-edge)
320
- * traefik.yml its static configuration
321
- * dynamic/<project>.yml one per stack: its routers, services, middlewares
322
- * certs/<project>.crt|.key a stack's own certificate, where it has one
323
- */
324
- /** The edge's directory on this machine. */
325
- function edgeDir(env = process.env) {
326
- return (0, node_path.join)(require_run.gkmHome(env), "edge");
327
- }
328
- /** The edge's compose project, as docker is asked about it. */
329
- function edgeRef(dir, options = {}) {
330
- return {
331
- project: require_stack.EDGE_PROJECT,
332
- file: (0, node_path.join)(dir, "docker-compose.yml"),
333
- cwd: dir,
334
- ...options
335
- };
336
- }
337
- /**
338
- * Something other than the proxy a stack is set to holds the edge's ports.
339
- *
340
- * With `proxy: 'traefik'`, the stack's own Caddy — still running from before
341
- * the switch — or any other container; with `proxy: 'caddy'`, the shared
342
- * edge, or another stack's Caddy. Refused before anything is started, so the
343
- * stack is never left half on one proxy and half on the other.
344
- */
345
- var ComposeProxyClash = class extends require_errors.GkmError {
346
- project;
347
- proxy;
348
- port;
349
- holder;
350
- constructor(project, proxy, port, holder) {
351
- const by = holder.project ? `${holder.container} (compose project ${holder.project})` : holder.container;
352
- const fix = proxy === "traefik" ? holder.project === project && holder.service === "caddy" ? `That is this stack's own Caddy, from before it was switched to proxy: 'traefik'. Move it once: stop it (docker compose -p ${project} stop caddy), then run gkm compose again — the shared edge takes 80/443 and the stack registers with it.` : `The shared edge needs ports 80 and 443. Stop ${holder.container}${holder.project ? ` (docker compose -p ${holder.project} down, or its own gkm compose --down)` : ""}, or move the edge with GKM_COMPOSE_HTTPS_PORT/GKM_COMPOSE_HTTP_PORT.` : holder.project === "gkm-edge" ? `That is the server's shared Traefik edge, which other stacks register with. Set deploy.compose.proxy to 'traefik' for this stage so the stack registers with it too, or stop the edge once nothing uses it (docker compose -p ${require_stack.EDGE_PROJECT} down).` : `Stop ${holder.container}, or publish this stack's Caddy elsewhere with GKM_COMPOSE_HTTPS_PORT/GKM_COMPOSE_HTTP_PORT.`;
353
- super(`${project} is set to proxy: '${proxy}', and port ${port} is held by ${by}. ${fix}`);
354
- this.project = project;
355
- this.proxy = proxy;
356
- this.port = port;
357
- this.holder = holder;
358
- this.name = "ComposeProxyClash";
359
- }
360
- };
361
- /**
362
- * Refuse a stack whose edge ports are held by anything but its own proxy:
363
- * the shared edge for `'traefik'`, the stack's own Caddy for `'caddy'`.
364
- */
365
- async function assertEdgePorts(docker, options) {
366
- const { project, proxy, ports } = options;
367
- for (const port of [ports.https, ports.http]) for (const holder of await docker.publishers(port)) if (!(proxy === "traefik" ? holder.project === "gkm-edge" : holder.project === project && holder.service === "caddy")) throw new ComposeProxyClash(project, proxy, port, holder);
368
- }
369
- /** Write a file whole or not at all: a temporary file, then a rename. */
370
- async function writeAtomic(path, content, mode = 420) {
371
- const temporary = `${path}.${process.pid}.tmp`;
372
- await (0, node_fs_promises.writeFile)(temporary, content, { mode });
373
- await (0, node_fs_promises.chmod)(temporary, mode);
374
- await (0, node_fs_promises.rename)(temporary, path);
375
- }
376
- /** Write a file only when its content changed; whether it did. */
377
- async function writeIfChanged(path, content) {
378
- if (await (0, node_fs_promises.readFile)(path, "utf-8").catch(() => void 0) === content) return false;
379
- await writeAtomic(path, content);
380
- return true;
381
- }
382
- /**
383
- * The edge, up: its network, its files, and its container — started when it
384
- * is not running, and recreated when its configuration changed. Idempotent:
385
- * on a server where it already runs as configured, `up` changes nothing.
386
- */
387
- async function ensureEdge(docker, options) {
388
- const { dir, ports } = options;
389
- await (0, node_fs_promises.mkdir)((0, node_path.join)(dir, "dynamic"), { recursive: true });
390
- await (0, node_fs_promises.mkdir)((0, node_path.join)(dir, "certs"), {
391
- recursive: true,
392
- mode: 448
393
- });
394
- const staticConfig = require_stack.traefikStaticFile(ports);
395
- await writeIfChanged((0, node_path.join)(dir, "traefik.yml"), staticConfig);
396
- await writeIfChanged((0, node_path.join)(dir, "docker-compose.yml"), require_stack.edgeComposeFile({
397
- dir,
398
- ports,
399
- staticConfig,
400
- logging: options.logging
401
- }));
402
- await docker.ensureNetwork(require_stack.EDGE_NETWORK);
403
- await docker.up(edgeRef(dir, {
404
- ...options.output ? { output: options.output } : {},
405
- ...options.signal ? { signal: options.signal } : {}
406
- }), [require_stack.EDGE_SERVICE]);
407
- return [(0, node_path.join)(dir, "traefik.yml"), (0, node_path.join)(dir, "docker-compose.yml")];
408
- }
409
- /** A stack's file in the edge's directory. */
410
- function routesFile(dir, project) {
411
- return (0, node_path.join)(dir, "dynamic", `${project}.yml`);
412
- }
413
- /**
414
- * Register a stack with the edge: its certificate first, where it has one,
415
- * so the routes that need it never load without it — then its routes, in one
416
- * rename the edge picks up whole.
417
- */
418
- async function writeEdgeRoutes(dir, project, routes, certificate) {
419
- const files = [];
420
- if (certificate) {
421
- const certs = (0, node_path.join)(dir, "certs");
422
- await (0, node_fs_promises.mkdir)(certs, {
423
- recursive: true,
424
- mode: 448
425
- });
426
- const crt = (0, node_path.join)(certs, `${project}.crt`);
427
- const key = (0, node_path.join)(certs, `${project}.key`);
428
- await (0, node_fs_promises.copyFile)(certificate.certFile, `${crt}.tmp`);
429
- await (0, node_fs_promises.rename)(`${crt}.tmp`, crt);
430
- await (0, node_fs_promises.copyFile)(certificate.keyFile, `${key}.tmp`);
431
- await (0, node_fs_promises.chmod)(`${key}.tmp`, 384);
432
- await (0, node_fs_promises.rename)(`${key}.tmp`, key);
433
- files.push(crt, key);
434
- } else await removeCertificate(dir, project);
435
- await (0, node_fs_promises.mkdir)((0, node_path.join)(dir, "dynamic"), { recursive: true });
436
- const file = routesFile(dir, project);
437
- await writeAtomic(file, routes);
438
- files.push(file);
439
- return files;
440
- }
441
- /** What a stack's file holds for the moment between emptying and removal. */
442
- const UNREGISTERED = "# Unregistered by gkm compose --down.\n";
443
- async function removeCertificate(dir, project) {
444
- for (const ext of ["crt", "key"]) await (0, node_fs_promises.rm)((0, node_path.join)(dir, "certs", `${project}.${ext}`), { force: true });
445
- }
446
- /**
447
- * Unregister a stack: its routes, then its certificate. Whether there was
448
- * anything to remove.
449
- */
450
- async function removeEdgeRoutes(dir, project) {
451
- const file = routesFile(dir, project);
452
- const existed = (0, node_fs.existsSync)(file);
453
- if (existed) await writeAtomic(file, UNREGISTERED);
454
- await (0, node_fs_promises.rm)(file, { force: true });
455
- await removeCertificate(dir, project);
456
- return existed;
457
- }
458
- //#endregion
459
- //#region src/target/compose/health.ts
460
- /**
461
- * Asking each app of a stack whether it answers — through Caddy, over HTTPS,
462
- * the way a browser reaches it.
463
- *
464
- * `docker compose up --wait` already knows each container passed its own
465
- * health check, asked on 127.0.0.1 inside it. What that cannot say is whether
466
- * the stack is reachable: whether the edge routes the host to the app, and
467
- * whether the certificate it presents is one a client accepts. So the check
468
- * goes through the edge, by hostname, and verifies the certificate.
469
- */
470
- /** A request that got no answer in time. */
471
- var HealthRequestTimedOut = class extends require_errors.GkmError {
472
- url;
473
- timeoutMs;
474
- constructor(url, timeoutMs) {
475
- super(`${url} did not answer within ${timeoutMs}ms. Check the stack is up with \`docker compose ps\`.`);
476
- this.url = url;
477
- this.timeoutMs = timeoutMs;
478
- this.name = "HealthRequestTimedOut";
479
- }
480
- };
481
- /** The real probe: one HTTPS GET, the certificate verified. */
482
- const httpsProbe = ({ url, ca, connectTo, connectPort, timeoutMs, signal }) => {
483
- const target = new URL(url);
484
- return new Promise((resolve, reject) => {
485
- const req = (0, node_https.request)({
486
- host: connectTo ?? target.hostname,
487
- port: connectPort ?? (target.port || 443),
488
- servername: target.hostname,
489
- path: `${target.pathname}${target.search}`,
490
- method: "GET",
491
- headers: { host: target.host },
492
- ...ca ? { ca } : {},
493
- ...signal ? { signal } : {},
494
- timeout: timeoutMs
495
- }, (res) => {
496
- res.resume();
497
- res.on("end", () => resolve(res.statusCode ?? 0));
498
- res.on("error", reject);
499
- });
500
- req.on("timeout", () => req.destroy(new HealthRequestTimedOut(url, timeoutMs)));
501
- req.on("error", reject);
502
- req.end();
503
- });
504
- };
505
- /** Whether a status means the app answered as it should. */
506
- function isHealthy(status) {
507
- return status >= 200 && status < 400;
508
- }
509
- /** Apps that did not answer through the edge. */
510
- var ComposeAppsUnhealthy = class extends require_errors.GkmError {
511
- project;
512
- apps;
513
- proxy;
514
- constructor(project, apps, proxy = "caddy") {
515
- const names = apps.map((a) => a.app).join(" ");
516
- super(`${project} started, but ${apps.length === 1 ? "this app does" : "these apps do"} not answer through ${proxy === "caddy" ? "Caddy" : "the shared Traefik edge"}:\n` + apps.map(({ url, last }) => ` - ${url}: ${last}`).join("\n") + (proxy === "caddy" ? `\nSee why with \`docker compose -p ${project} logs caddy ${names}\`.` : `\nSee why with \`docker compose -p gkm-edge logs traefik\` and \`docker compose -p ${project} logs ${names}\`.`));
517
- this.project = project;
518
- this.apps = apps;
519
- this.proxy = proxy;
520
- this.name = "ComposeAppsUnhealthy";
521
- }
522
- };
523
- //#endregion
524
- //#region src/target/compose/phases.ts
525
- /**
526
- * The compose target's phases: a stage as one Docker Compose stack behind
527
- * Caddy, on the machine the deploy runs on.
528
- *
529
- * The order is the point of this file, and the rules it keeps are these:
530
- *
531
- * - A tag is checked before anything happens (`validate`). Every app's image
532
- * is asked of the registry first; one missing image stops the run naming
533
- * them all, and nothing is written, pulled or started.
534
- * - The databases exist before the apps do (`provision`). Postgres comes up
535
- * alone, its databases, roles and grants are created, its migrations
536
- * applied and its seeds run from this machine, and only then do the apps
537
- * start — so an app
538
- * never boots against a schema that is not there yet.
539
- * - What a stage runs is recorded (`release`). Each app's image, the tag it
540
- * was released under and the digest it resolved to are written to the
541
- * stage's state, so "what is this stage running" has an exact answer.
542
- * - What was started answers (`verify`): each app is asked through Caddy,
543
- * over HTTPS, with the certificate verified.
544
- *
545
- * Every image is built inside Docker from a pruned slice of the build root —
546
- * the same Dockerfiles `gkm docker` writes — so nothing is built on this
547
- * machine before `docker compose build`.
548
- *
549
- * Docker, the registry, Postgres and the health probe are
550
- * injected, so the rules are asserted without a daemon; the defaults are the
551
- * real ones.
552
- */
553
- /** `--push` without `--build`: there is nothing built here to push. */
554
- var ComposePushNeedsBuild = class extends require_errors.GkmError {
555
- constructor() {
556
- super("--push pushes the images this checkout builds, so it needs --build (and never --pull): gkm compose --stage <stage> --build --push --tag <tag>.");
557
- this.name = "ComposePushNeedsBuild";
558
- }
559
- };
560
- /** Digests to run by, on a run that pulls nothing. */
561
- var ComposePinNeedsPull = class extends require_errors.GkmError {
562
- constructor() {
563
- super("--digests-file pins the images a release pulls, so it needs --tag (or --pull) — or, with --build --push, it is where the pushed digests are written.");
564
- this.name = "ComposePinNeedsPull";
565
- }
566
- };
567
- /** A port variable that does not hold a port. */
568
- var EdgePortInvalid = class extends require_errors.GkmError {
569
- variable;
570
- value;
571
- constructor(variable, value) {
572
- super(`${variable}=${value} is not a port. Set it to a number between 1 and 65535, or unset it for the default.`);
573
- this.variable = variable;
574
- this.value = value;
575
- this.name = "EdgePortInvalid";
576
- }
577
- };
578
- /** A build with no commit to tag its images with. */
579
- var NoGitRevision = class extends require_errors.GkmError {
580
- root;
581
- constructor(root) {
582
- super(`Images built here are tagged with the commit they were built from, and ${root} is not in a git repository with a commit. Commit, or pass --tag to name the images yourself.`);
583
- this.root = root;
584
- this.name = "NoGitRevision";
585
- }
586
- };
587
- /**
588
- * The project's own file merged over a stage's stack: `docker-compose.<stage>.yml`
589
- * at the workspace root. What the generated file cannot know — a port bound
590
- * to a tailnet address, another log driver — goes there, and is never
591
- * overwritten.
592
- */
593
- function stackOverrideFile(root, stage) {
594
- return (0, node_path.join)(root, `docker-compose.${stage}.yml`);
595
- }
596
- /** The commit HEAD is at, short — and `-dirty` when the tree has changes. */
597
- async function gitRevision(root) {
598
- let sha;
599
- try {
600
- sha = (await require_run.runOutput("git", [
601
- "rev-parse",
602
- "--short",
603
- "HEAD"
604
- ], { cwd: root })).trim();
605
- } catch {
606
- throw new NoGitRevision(root);
607
- }
608
- if (!sha) throw new NoGitRevision(root);
609
- return (await require_run.runOutput("git", ["status", "--porcelain"], { cwd: root }).catch(() => "")).trim() ? `${sha}-dirty` : sha;
610
- }
611
- /** The edge's ports, from the environment — 443 and 80 when unset. */
612
- function edgePorts(env = process.env) {
613
- const read = (variable, fallback) => {
614
- const value = env[variable];
615
- if (value === void 0 || value === "") return fallback;
616
- const port = Number(value);
617
- if (!Number.isInteger(port) || port < 1 || port > 65535) throw new EdgePortInvalid(variable, value);
618
- return port;
619
- };
620
- const logs = env["GKM_COMPOSE_LOGS_PORT"] ? read(require_stack.LOGS_PORT_ENV, 0) : void 0;
621
- return {
622
- https: read(require_stack.EDGE_PORT_ENV.https, 443),
623
- http: read(require_stack.EDGE_PORT_ENV.http, 80),
624
- ...logs ? { logs } : {}
625
- };
626
- }
627
- /**
628
- * The real ones. Each app is asked for three minutes before it counts as
629
- * down: a deployed stage's first ACME certificate can take that long.
630
- */
631
- const defaultDeps = {
632
- docker: dockerCompose,
633
- revision: gitRevision,
634
- sql: require_seeds.pgClient,
635
- buckets: require_seeds.bucketClient,
636
- logins: require_seeds.ensurePostgresLogin,
637
- migrate: require_databases.migrateDatabases,
638
- seed: require_databases.seedDatabases,
639
- probe: httpsProbe,
640
- healthAttempts: 90,
641
- healthIntervalMs: 2e3,
642
- env: process.env,
643
- lookup: require_dns.systemLookup
644
- };
645
- /**
646
- * The stack the stage would run, and — for a tag — the registry's word that
647
- * every image it names exists. Nothing is written: a missing image stops the
648
- * run here, before a file, a secret or a container is touched.
649
- */
650
- async function validateCompose(ctx, deps, options = {}) {
651
- const { workspace, stage, identity, tag } = ctx;
652
- const root = workspace.root;
653
- const mode = options.mode ?? (ctx.tagGiven ? "pull" : "build");
654
- const push = options.push === true;
655
- if (push && mode !== "build") throw new ComposePushNeedsBuild();
656
- if (options.pin && mode !== "pull") throw new ComposePinNeedsPull();
657
- const registry = workspace.deploy?.registry;
658
- if (!registry && (push || mode === "pull")) throw new require_stack.RegistryRequired(push ? "push" : "pull", require_seeds.imageRef(identity, ctx.apps[0] ?? workspace.name, void 0, tag));
659
- const runnables = {};
660
- const background = {};
661
- const manifest = await require_discover.discover({
662
- patterns: require_seeds.constructGlobs(workspace),
663
- cwd: root,
664
- runnables,
665
- background
666
- });
667
- const { secrets, generated } = await stageSecrets(ctx, manifest);
668
- const layout = mode === "build" ? require_seeds.imageLayout(workspace) : void 0;
669
- const composed = require_stack.composeStack({
670
- workspace,
671
- manifest,
672
- runnables,
673
- background,
674
- stage,
675
- identity,
676
- images: {
677
- mode,
678
- tag,
679
- ...registry ? { registry } : {}
680
- },
681
- secrets,
682
- ...stage === workspace.stages.local ? { localCredentials: (await require_seeds.loadLocalCredentials(workspace)).credentials } : {},
683
- allowDevServices: ctx.allowDevServices,
684
- ports: edgePorts(deps.env),
685
- ...layout ? { layout } : {},
686
- ...push ? { buildOnly: true } : {}
687
- });
688
- if (mode === "build") assertRedisClient(workspace, composed);
689
- const stack = options.pin ? withPinnedImages(composed, options.pin.file, options.pin.digests) : composed;
690
- if (mode === "pull" && !ctx.dryRun) await require_stack.assertImagesExist(deps.docker, tag, [...stack.apps, ...stack.workers]);
691
- if (stack.redis && !stack.local) ctx.secrets.mask(stack.redis.password);
692
- if (stack.logs && (!stack.local || stack.logs.passwordFromSecrets)) {
693
- ctx.secrets.mask(stack.logs.password);
694
- ctx.secrets.mask(stack.logs.appEnv.OTEL_EXPORTER_OTLP_HEADERS);
695
- }
696
- const dir = (0, node_path.join)(root, require_stack.stackDir(stage));
697
- const override = stackOverrideFile(root, stage);
698
- const overrides = (0, node_fs.existsSync)(override) ? [override] : [];
699
- if (overrides.length > 0) ctx.logger.info(`🧩 Merging docker-compose.${stage}.yml over the generated stack`);
700
- const recorded = push ? void 0 : await ctx.state.read(stage);
701
- const previous = Object.fromEntries(Object.entries(recorded?.state.releases ?? {}).map(([name, releases]) => {
702
- const { ref, tag, digest } = releases.current;
703
- return [name, {
704
- ref,
705
- ...tag ? { tag } : {},
706
- ...digest ? { digest } : {}
707
- }];
708
- }));
709
- return {
710
- mode,
711
- push,
712
- stack,
713
- dir,
714
- ref: {
715
- project: require_stack.composeProject(identity),
716
- file: (0, node_path.join)(dir, "docker-compose.yml"),
717
- ...overrides.length > 0 ? { overrides } : {},
718
- cwd: root,
719
- output: ctx.childOutput,
720
- signal: ctx.signal
721
- },
722
- secrets,
723
- generated,
724
- previous,
725
- files: [],
726
- images: {},
727
- changes: []
728
- };
729
- }
730
- /**
731
- * What only a compose deploy checks before it changes anything, once the
732
- * stage's own keys are: the stage's own certificate is there, and every
733
- * public host points at the stage's server — its records written where a
734
- * provider holds them, then confirmed — before Caddy or Traefik ask Let's
735
- * Encrypt for a certificate it could not issue. A build-only run never asks.
736
- */
737
- async function readyCompose(ctx, run, deps) {
738
- const { stack } = run;
739
- if (stack.tls) {
740
- for (const file of [stack.tls.certFile, stack.tls.keyFile]) if (!(0, node_fs.existsSync)(file)) throw new require_workspace.ComposeTlsFileMissing(ctx.stage, file);
741
- }
742
- if (stack.local) return;
743
- await stageHostsDns(ctx, deps, require_dns.stackHosts(stack).filter((host) => !require_dns.isLocalHost(host)), run.secrets?.custom);
744
- }
745
- /** A stack whose caches are in Redis, built for an app that has no client. */
746
- var RedisClientMissing = class extends require_errors.GkmError {
747
- apps;
748
- constructor(apps) {
749
- super(`The stack keeps every cache in Redis, and ${apps.join(", ")} ${apps.length === 1 ? "does" : "do"} not depend on ioredis, the client the generated server reaches it with. Add it to the workspace: pnpm add ioredis`);
750
- this.apps = apps;
751
- this.name = "RedisClientMissing";
752
- }
753
- };
754
- /**
755
- * Every backend and worker image built for a stack with a cache can resolve
756
- * `ioredis` — listed by its app or a directory above it, the way Node and
757
- * the image's install find it.
758
- */
759
- function assertRedisClient(workspace, stack) {
760
- if (!stack.plan.resources.some((r) => r.kind === "cache")) return;
761
- const missing = [.../* @__PURE__ */ new Set([...stack.apps.filter((app) => app.kind === "rest-api").map((a) => a.name), ...stack.workers.map((worker) => worker.host)])].filter((name) => {
762
- const app = workspace.apps[name];
763
- return app && !require_drivers.installedFrom((0, node_path.join)(workspace.root, app.path), "ioredis");
764
- }).sort();
765
- if (missing.length > 0) throw new RedisClientMissing(missing);
766
- }
767
- /**
768
- * The stage's secrets — and, for a deployed stage, everything it generates
769
- * once: its seed, and each declared secret and keyring. Kept only once the
770
- * run goes ahead (`provision`), so a dry run, or a tag that is not there,
771
- * leaves the stage's secrets as it found them.
772
- */
773
- /**
774
- * The deploy's DNS step, for a deployed stage: its server's address — required
775
- * where it serves a real domain — then its own hosts written through each
776
- * domain's provider in `dns` where they differ, read back from it, and
777
- * recorded in the stage's state; hosts no provider writes resolved with the
778
- * system resolver. A dry run prints the plan and writes nothing, and warns
779
- * rather than stops. What a deploy (`ready`) and `--resources-only` both run.
780
- *
781
- * @throws {ServerAddressMissing} when the stage has a domain and no address
782
- * @throws {ServerAddressInvalid} for a value that is not an address
783
- * @throws {HostNotPointingAtServer} when a host is not the server
784
- */
785
- async function stageHostsDns(ctx, deps, hosts, custom) {
786
- const { workspace, stage } = ctx;
787
- const server = require_dns.requiredServerAddress(stage, workspace.domains?.[stage], custom);
788
- if (hosts.length === 0) return;
789
- if (ctx.skipDns) {
790
- ctx.logger.info("🌐 DNS skipped (--skip-dns): no record is written or checked");
791
- return;
792
- }
793
- if (ctx.skipResources) {
794
- ctx.logger.info("🌐 DNS skipped (--skip-resources): the --resources-only run wrote and confirmed the records");
795
- return;
796
- }
797
- if (!server) {
798
- ctx.logger.info(`🌐 DNS skipped: no ${require_workspace.SERVER_IPV4_KEY} in '${stage}''s secrets — ${require_dns.serverAddressHint(stage)}`);
799
- return;
800
- }
801
- const log = (line) => ctx.logger.info(line);
802
- if (workspace.dns) log(`🌐 DNS for '${stage}' → ${server.ipv4}${server.ipv6 ? ` / ${server.ipv6}` : ""}${ctx.dryRun ? " (dry run — nothing is written)" : ""}`);
803
- try {
804
- const result = await require_dns.stageDns({
805
- stage,
806
- hosts,
807
- server,
808
- dns: workspace.dns,
809
- credentials: ctx.credentials,
810
- log,
811
- lookup: deps.lookup ?? require_dns.systemLookup,
812
- ...ctx.dryRun ? { dryRun: true } : {},
813
- ...deps.dnsProviderFor ? { providerFor: deps.dnsProviderFor } : {}
814
- });
815
- if (!ctx.dryRun) {
816
- const journal = await require_seeds.DeployJournal.open(ctx.state, stage, () => require_seeds.createComposeState(stage));
817
- const providers = new Map(result.plan.domains.map((d) => [d.domain, d.provider]));
818
- await require_seeds.recordDnsChanges(journal, result.changes, (domain) => providers.get(domain));
819
- }
820
- const parts = [result.confirmed.length > 0 ? `${result.confirmed.length} host${result.confirmed.length === 1 ? "" : "s"} confirmed by the DNS provider` : "", result.resolved.length > 0 ? `${result.resolved.length} host${result.resolved.length === 1 ? "" : "s"} resolve${result.resolved.length === 1 ? "s" : ""} to ${server.ipv4}` : ""].filter(Boolean);
821
- if (parts.length > 0) log(`🌐 ${parts.join(", ")}`);
822
- } catch (error) {
823
- if (ctx.dryRun && error instanceof require_dns.HostNotPointingAtServer) {
824
- ctx.logger.warn(`⚠ ${error.message}`);
825
- return;
826
- }
827
- throw error;
828
- }
829
- }
830
- /**
831
- * `--resources-only`: the stage's DNS records, from the hosts its stack would
832
- * serve — composed in memory, nothing written, no image asked for. What a CI
833
- * runner runs with the DNS provider's token, so the server never holds it.
834
- */
835
- async function resourcesCompose(ctx, deps) {
836
- const { workspace, stage } = ctx;
837
- if (stage === workspace.stages.local) return;
838
- const stored = await ctx.secrets.read();
839
- const { composeStageHosts } = await Promise.resolve().then(() => require("./dns-4RAiVFa7.cjs")).then((n) => n.dns_exports);
840
- await stageHostsDns(ctx, deps, (await composeStageHosts({
841
- workspace,
842
- stage,
843
- stored,
844
- manifest: ctx.manifest,
845
- runnables: ctx.runnables ?? {},
846
- background: ctx.background ?? {}
847
- })).filter((host) => !require_dns.isLocalHost(host)), stored?.custom);
848
- }
849
- async function stageSecrets(ctx, manifest) {
850
- const stored = await ctx.secrets.read();
851
- if (ctx.stage === ctx.workspace.stages.local) return {
852
- secrets: stored,
853
- generated: []
854
- };
855
- const { secrets, generated } = require_secrets.deployedStackSecrets(ctx.workspace, ctx.stage, stored, manifest);
856
- if (secrets.seed) ctx.secrets.mask(secrets.seed);
857
- for (const value of Object.values(secrets.custom ?? {})) ctx.secrets.mask(value);
858
- return {
859
- secrets,
860
- generated
861
- };
862
- }
863
- /**
864
- * The stack with each app's and worker's image pinned to the digest the file
865
- * names: the refs it is checked, pulled and recorded by, and the image each
866
- * compose service runs.
867
- */
868
- function withPinnedImages(stack, file, digests) {
869
- const apps = require_stack.pinImages(file, digests, stack.apps);
870
- const workers = require_stack.pinImages(file, digests, stack.workers);
871
- const services = { ...stack.compose.services };
872
- for (const image of [...apps, ...workers]) services[image.name] = {
873
- ...services[image.name],
874
- image: image.ref
875
- };
876
- return {
877
- ...stack,
878
- apps,
879
- workers,
880
- compose: {
881
- ...stack.compose,
882
- services
883
- }
884
- };
885
- }
886
- /**
887
- * A dry run: the stack's files, written so they can be read, and what a real
888
- * run would do — no image is built or pulled, no container touched, nothing
889
- * recorded and no secret kept.
890
- */
891
- async function planCompose(ctx, run) {
892
- if (run.generated.length > 0) ctx.logger.info(`🔑 "${ctx.stage}" has no ${run.generated.join(", ")} yet; this dry run used values it did not keep.`);
893
- run.files = await writeStack(ctx.cwd, run.dir, run.stack);
894
- printPlan(ctx, run);
895
- if (!run.push && run.stack.infra.includes("postgres")) require_seeds.reportPlannedSeeds((await require_databases.plannedSeeds({
896
- root: ctx.workspace.root,
897
- manifest: ctx.manifest
898
- })).map(({ target, seeds }) => ({
899
- folder: target.seeds,
900
- seeds
901
- })), (line) => ctx.logger.info(line));
902
- const planned = (change) => {
903
- run.changes.push(change);
904
- ctx.emit({
905
- type: "resource.planned",
906
- ...change
907
- });
908
- };
909
- for (const service of run.push ? [] : run.stack.infra) planned({
910
- key: `service:${service}`,
911
- resourceType: "service",
912
- action: "ensure"
913
- });
914
- for (const app of run.stack.apps) {
915
- planned({
916
- key: `image:${app.name}`,
917
- resourceType: "image",
918
- action: run.mode === "build" ? "build" : "reuse",
919
- id: app.ref
920
- });
921
- if (run.push) continue;
922
- planned({
923
- key: `service:${app.name}`,
924
- resourceType: "service",
925
- action: run.previous[app.name] ? "reuse" : "create"
926
- });
927
- }
928
- for (const worker of run.stack.workers) {
929
- planned({
930
- key: `image:${worker.name}`,
931
- resourceType: "image",
932
- action: run.mode === "build" ? "build" : "reuse",
933
- id: worker.ref
934
- });
935
- if (run.push) continue;
936
- planned({
937
- key: `service:${worker.name}`,
938
- resourceType: "service",
939
- action: run.previous[worker.name] ? "reuse" : "create"
940
- });
941
- }
942
- if (!run.push) planned({
943
- key: run.stack.proxy === "caddy" ? "service:caddy" : `service:${require_stack.EDGE_PROJECT}/${require_stack.EDGE_SERVICE}`,
944
- resourceType: "service",
945
- action: "ensure"
946
- });
947
- }
948
- /**
949
- * What the apps run on: the stage's generated secrets kept, the stack's files
950
- * written, its infrastructure started, and its databases, roles, grants and
951
- * migrations applied.
952
- */
953
- async function provisionCompose(ctx, run, deps) {
954
- const { stack, ref } = run;
955
- if (run.push) return;
956
- if (run.generated.length > 0 && run.secrets) {
957
- await ctx.secrets.write(run.secrets);
958
- ctx.logger.info(`🔑 Generated for "${ctx.stage}" (${ctx.secrets.store}): ${run.generated.join(", ")}`);
959
- }
960
- const ports = edgePorts(deps.env);
961
- await assertEdgePorts(deps.docker, {
962
- project: stack.project,
963
- proxy: stack.proxy,
964
- ports
965
- });
966
- run.files = await writeStack(ctx.cwd, run.dir, stack);
967
- printPlan(ctx, run);
968
- if (stack.proxy === "traefik") {
969
- const dir = edgeDir(deps.env);
970
- ctx.logger.info(`\n🌐 Starting the shared edge (${require_stack.EDGE_PROJECT}) from ${dir}…`);
971
- run.files.push(...await ensureEdge(deps.docker, {
972
- dir,
973
- ports,
974
- logging: {
975
- driver: require_stack.LOG_ROTATION.driver,
976
- options: { ...require_stack.LOG_ROTATION.options }
977
- },
978
- ...ref.output ? { output: ref.output } : {},
979
- ...ref.signal ? { signal: ref.signal } : {}
980
- }));
981
- applied(ctx, run, {
982
- key: `service:${require_stack.EDGE_PROJECT}/${require_stack.EDGE_SERVICE}`,
983
- resourceType: "service",
984
- action: "ensure",
985
- id: `${require_stack.EDGE_PROJECT}/${require_stack.EDGE_SERVICE}`,
986
- via: "found"
987
- });
988
- }
989
- if (stack.infra.length > 0) {
990
- ctx.logger.info(`\n🗄️ Starting ${stack.infra.join(", ")}…`);
991
- await deps.docker.up(ref, stack.infra);
992
- for (const service of stack.infra) applied(ctx, run, {
993
- key: `service:${service}`,
994
- resourceType: "service",
995
- action: "ensure",
996
- id: `${stack.project}/${service}`,
997
- via: Object.keys(run.previous).length > 0 ? "recorded" : "created"
998
- });
999
- }
1000
- if (stack.infra.includes("postgres")) await prepareDatabases(ctx, run, deps);
1001
- if (stack.storage && stack.infra.includes("minio")) await prepareBuckets(ctx, run, deps);
1002
- }
1003
- /**
1004
- * Create each bucket the stack's MinIO serves, and the open paths of each
1005
- * one a file server fronts — from this machine, on MinIO's loopback port,
1006
- * the way `gkm dev` creates them.
1007
- */
1008
- async function prepareBuckets(ctx, run, deps) {
1009
- const storage = run.stack.storage;
1010
- if (storage.buckets.length === 0) return;
1011
- const port = await deps.docker.port(run.ref, "minio", 9e3);
1012
- const client = deps.buckets(port, storage);
1013
- const created = [...await require_seeds.applyBuckets(client, storage.buckets), ...await require_seeds.applyPolicies(client, storage.policies)].filter((entry) => entry.created).length;
1014
- ctx.logger.info(`🪣 Buckets: ${storage.buckets.join(", ")}${created ? ` (${created} change(s))` : ""}`);
1015
- }
1016
- /**
1017
- * Create what the plan names in the stack's Postgres, migrate it, then run
1018
- * its seeds — every one, every time, as each construct's owner.
1019
- */
1020
- async function prepareDatabases(ctx, run, deps) {
1021
- const { stack } = run;
1022
- const workspace = ctx.workspace;
1023
- const port = await deps.docker.port(run.ref, "postgres", 5432);
1024
- const superuser = stack.credential.containers.postgres;
1025
- const login = await deps.logins({
1026
- port,
1027
- login: superuser,
1028
- legacy: [{
1029
- user: require_seeds.LEGACY_LOCAL_LOGIN.user,
1030
- password: superuser.password
1031
- }, ...stack.local ? [require_seeds.LEGACY_LOCAL_LOGIN] : []]
1032
- });
1033
- if (login.notice) ctx.logger.info(login.notice);
1034
- const statements = require_seeds.postgresStatements(stack.plan, workspace.name, stack.credential.seed);
1035
- if (statements.length > 0) {
1036
- ctx.logger.info("🗄️ Creating databases, roles and grants…");
1037
- await require_seeds.applyPostgres(deps.sql(port, login.login), statements);
1038
- }
1039
- const sources = {};
1040
- const manifest = await require_discover.discover({
1041
- patterns: require_seeds.constructGlobs(workspace),
1042
- cwd: workspace.root,
1043
- sources
1044
- });
1045
- const env = require_seeds.envFor(stack.plan, {
1046
- ports: { [require_seeds.primaryPortKey("postgres")]: port },
1047
- project: workspace.name,
1048
- credentials: {
1049
- ...stack.credential.containers,
1050
- postgres: login.login
1051
- },
1052
- seed: stack.credential.seed
1053
- });
1054
- const options = {
1055
- root: workspace.root,
1056
- manifest,
1057
- sources,
1058
- env
1059
- };
1060
- const runs = await deps.migrate(options);
1061
- const seeds = await deps.seed({
1062
- ...options,
1063
- stage: ctx.stage
1064
- }).catch((error) => {
1065
- if (error instanceof require_databases.SeedFailed) throw new require_seeds.DeploySeedsFailed(ctx.stage, error.construct, error.seed, error);
1066
- throw error;
1067
- });
1068
- require_seeds.reportDatabaseRuns(runs.map(({ target, applied }) => ({
1069
- construct: target.id,
1070
- folder: target.migrations,
1071
- applied
1072
- })), seeds.map(({ target, seeded }) => ({
1073
- construct: target.id,
1074
- folder: target.seeds,
1075
- seeded
1076
- })), (line) => ctx.logger.info(line), (event) => ctx.emit(event));
1077
- }
1078
- /**
1079
- * Each app's image: built from this checkout — inside Docker, every one of
1080
- * them, from a pruned slice of the build root — or pulled at the tag. Nothing
1081
- * live changes.
1082
- */
1083
- async function buildCompose(ctx, run, deps) {
1084
- const { stack, ref } = run;
1085
- const images = [...stack.apps, ...stack.workers];
1086
- const apps = images.map((image) => image.app);
1087
- if (run.push) {
1088
- run.files = await writeStack(ctx.cwd, run.dir, stack);
1089
- printPlan(ctx, run);
1090
- }
1091
- if (run.mode === "build") {
1092
- ctx.logger.info("\n🐳 Building images…");
1093
- await deps.docker.build(ref, apps);
1094
- } else {
1095
- ctx.logger.info(`\n🐳 Pulling ${ctx.tag}…`);
1096
- await deps.docker.pull(ref, apps);
1097
- }
1098
- if (run.push) ctx.logger.info("\n📤 Pushing images…");
1099
- for (const image of images) {
1100
- const digest = run.push ? await deps.docker.push(ref, image.ref) : await deps.docker.digest(image.ref);
1101
- if (run.push) ctx.logger.info(` ${image.app.padEnd(12)} ${image.ref}@${digest}`);
1102
- run.images[image.app] = {
1103
- ref: image.ref,
1104
- tag: image.tag,
1105
- ...digest ? { digest } : {}
1106
- };
1107
- ctx.emit({
1108
- type: "artifact.built",
1109
- app: image.app,
1110
- imageRef: image.ref,
1111
- ...digest ? { digest } : {}
1112
- });
1113
- run.changes.push({
1114
- key: `image:${image.app}`,
1115
- resourceType: "image",
1116
- action: run.mode === "build" ? "build" : "reuse",
1117
- id: digest ?? image.ref
1118
- });
1119
- }
1120
- }
1121
- /**
1122
- * The whole stack up — anything it no longer defines removed — and each
1123
- * app's image recorded in the stage's state.
1124
- */
1125
- async function releaseCompose(ctx, run, deps) {
1126
- const { stack, ref } = run;
1127
- if (run.push) return;
1128
- ctx.logger.info("\n🚀 Starting the stack…");
1129
- await deps.docker.up(ref);
1130
- const edge = edgeDir(deps.env);
1131
- if (stack.proxy === "traefik" && stack.traefik) {
1132
- const written = await writeEdgeRoutes(edge, stack.project, stack.traefik, stack.tls);
1133
- run.files.push(...written);
1134
- ctx.logger.info(`🌐 Registered ${stack.routes.length} route(s) with ${require_stack.EDGE_PROJECT}: ${written.at(-1)}`);
1135
- } else await removeEdgeRoutes(edge, stack.project);
1136
- if (stack.local) await deps.docker.copyOut(ref, "caddy", "/data/caddy/pki/authorities/local/root.crt", caFile(run)).catch(() => {});
1137
- await recordImages(ctx, run);
1138
- for (const app of stack.apps) {
1139
- const image = run.images[app.name];
1140
- applied(ctx, run, {
1141
- key: `service:${app.name}`,
1142
- resourceType: "service",
1143
- action: "ensure",
1144
- id: `${stack.project}/${app.name}`,
1145
- via: run.previous[app.name] ? "recorded" : "created"
1146
- });
1147
- ctx.emit({
1148
- type: "app.deployed",
1149
- app: app.name,
1150
- applicationId: `${stack.project}/${app.name}`,
1151
- imageRef: image?.ref ?? app.ref,
1152
- url: app.url
1153
- });
1154
- }
1155
- for (const worker of stack.workers) applied(ctx, run, {
1156
- key: `service:${worker.name}`,
1157
- resourceType: "service",
1158
- action: "ensure",
1159
- id: `${stack.project}/${worker.name}`,
1160
- via: run.previous[worker.name] ? "recorded" : "created"
1161
- });
1162
- ctx.logger.info(`\n✅ ${stack.project} is running:`);
1163
- for (const app of stack.apps) ctx.logger.info(` ${app.name.padEnd(12)} ${app.url}`);
1164
- for (const worker of stack.workers) ctx.logger.info(` ${worker.name.padEnd(12)} worker ${worker.id}`);
1165
- if (stack.local) ctx.logger.info(`\n🔐 Certificates are from Caddy's local CA: NODE_EXTRA_CA_CERTS=${(0, node_path.join)(require_stack.stackDir(ctx.stage), "caddy-root.crt")}`);
1166
- }
1167
- /**
1168
- * Each app's image in the stage's state — the same `images` a Dokploy deploy
1169
- * records: the ref it runs, the tag it was released under, and the digest
1170
- * the tag resolved to.
1171
- */
1172
- async function recordImages(ctx, run) {
1173
- const journal = await require_seeds.DeployJournal.open(ctx.state, ctx.stage, () => require_seeds.createComposeState(ctx.stage));
1174
- const by = require_seeds.currentActor();
1175
- for (const app of [...run.stack.apps, ...run.stack.workers]) {
1176
- const image = run.images[app.app] ?? {
1177
- ref: app.ref,
1178
- tag: app.tag
1179
- };
1180
- require_seeds.recordRelease(journal.state, app.app, image, by);
1181
- }
1182
- journal.state.identity = ctx.identity.key;
1183
- await journal.save();
1184
- }
1185
- /**
1186
- * Each app asked through Caddy, over HTTPS, the certificate verified: an API
1187
- * at `/health`, a site at `/`. On the local stage the edge is this machine and
1188
- * its certificates are Caddy's internal CA's, so the CA `release` copied out
1189
- * is the one trusted and the request goes to 127.0.0.1.
1190
- */
1191
- async function verifyCompose(ctx, run, deps) {
1192
- const { stack } = run;
1193
- if (run.push) return;
1194
- const ca = stack.local ? await (0, node_fs_promises.readFile)(caFile(run), "utf-8").catch(() => void 0) : void 0;
1195
- ctx.logger.info(`\n🩺 Checking each app through ${stack.proxy === "caddy" ? "Caddy" : "the shared edge"}…`);
1196
- const edge = stack.local || stack.proxy === "traefik" ? {
1197
- connectTo: "127.0.0.1",
1198
- ...stack.local ? {} : { connectPort: edgePorts(deps.env).https }
1199
- } : void 0;
1200
- const results = await Promise.all(stack.apps.map((app) => checkApp(ctx, app, deps, {
1201
- ca,
1202
- edge
1203
- })));
1204
- results.push(...await Promise.all(stack.workers.map((worker) => checkContainer(ctx, run, deps, worker.name))));
1205
- if (stack.logs) results.push(await checkContainer(ctx, run, deps, require_seeds.LOGS_SERVICE));
1206
- const down = results.filter((result) => !result.healthy).map(({ app, url, last }) => ({
1207
- app,
1208
- url,
1209
- last
1210
- }));
1211
- if (down.length > 0) throw new ComposeAppsUnhealthy(stack.project, down, stack.proxy);
1212
- if (stack.logs) reportLogs(ctx, stack.logs, stack.local);
1213
- }
1214
- /**
1215
- * A container's own health check, as Docker reports it: a worker's, which
1216
- * has no route through the edge, and OpenObserve's — by default it has no
1217
- * host there, and served publicly it answers only the addresses it allows,
1218
- * which need not include this one.
1219
- */
1220
- async function checkContainer(ctx, run, deps, service) {
1221
- const url = `docker:${service}`;
1222
- let last = "not asked";
1223
- for (let attempt = 1; attempt <= deps.healthAttempts; attempt++) {
1224
- ctx.signal.throwIfAborted();
1225
- const health = await deps.docker.health(run.ref, service).catch((error) => error instanceof Error ? error.message : String(error));
1226
- last = health ?? "not running";
1227
- const healthy = health === "healthy";
1228
- ctx.emit({
1229
- type: "health.checked",
1230
- app: service,
1231
- url,
1232
- healthy,
1233
- attempt
1234
- });
1235
- if (healthy) {
1236
- ctx.logger.info(` ✓ ${service.padEnd(12)} ${url} (${last})`);
1237
- return {
1238
- app: service,
1239
- url,
1240
- last,
1241
- healthy
1242
- };
1243
- }
1244
- if (attempt < deps.healthAttempts) await new Promise((resolve) => setTimeout(resolve, deps.healthIntervalMs));
1245
- }
1246
- ctx.logger.warn(` ✗ ${service.padEnd(12)} ${url} (${last})`);
1247
- return {
1248
- app: service,
1249
- url,
1250
- last,
1251
- healthy: false
1252
- };
1253
- }
1254
- /** How to open the logs, printed — and as an event, for a headless caller. */
1255
- function reportLogs(ctx, logs, local) {
1256
- const lines = require_seeds.logsAccess(logs, {
1257
- stage: ctx.stage,
1258
- local,
1259
- user: currentUser(),
1260
- hostname: (0, node_os.hostname)()
1261
- });
1262
- ctx.logger.info(`\n${lines.join("\n")}`);
1263
- ctx.emit({
1264
- type: "logs.ready",
1265
- service: require_seeds.LOGS_SERVICE,
1266
- access: logs.public ? "public" : "tunnel",
1267
- url: logs.url,
1268
- ...logs.public ? { allow: [...logs.public.allow] } : { port: logs.port },
1269
- email: logs.email
1270
- });
1271
- }
1272
- /** This machine's user — a guess at who would SSH in. */
1273
- function currentUser() {
1274
- try {
1275
- return (0, node_os.userInfo)().username;
1276
- } catch {
1277
- return process.env.USER ?? "user";
1278
- }
1279
- }
1280
- async function checkApp(ctx, app, deps, options) {
1281
- const url = `${app.url}${app.kind === "site" ? "/" : "/health"}`;
1282
- let last = "not asked";
1283
- for (let attempt = 1; attempt <= deps.healthAttempts; attempt++) {
1284
- ctx.signal.throwIfAborted();
1285
- let status;
1286
- try {
1287
- status = await deps.probe({
1288
- url,
1289
- ...options.ca ? { ca: options.ca } : {},
1290
- ...options.edge,
1291
- timeoutMs: 1e4,
1292
- signal: ctx.signal
1293
- });
1294
- last = `HTTP ${status}`;
1295
- } catch (error) {
1296
- last = error instanceof Error ? error.message : String(error);
1297
- }
1298
- const healthy = status !== void 0 && isHealthy(status);
1299
- ctx.emit({
1300
- type: "health.checked",
1301
- app: app.name,
1302
- url,
1303
- healthy,
1304
- ...status !== void 0 ? { status } : {},
1305
- attempt
1306
- });
1307
- if (healthy) {
1308
- ctx.logger.info(` ✓ ${app.name.padEnd(12)} ${url} (${last})`);
1309
- return {
1310
- app: app.name,
1311
- url,
1312
- last,
1313
- healthy
1314
- };
1315
- }
1316
- if (attempt < deps.healthAttempts) await new Promise((resolve) => setTimeout(resolve, deps.healthIntervalMs));
1317
- }
1318
- ctx.logger.warn(` ✗ ${app.name.padEnd(12)} ${url} (${last})`);
1319
- return {
1320
- app: app.name,
1321
- url,
1322
- last,
1323
- healthy: false
1324
- };
1325
- }
1326
- function composeResult(ctx, run) {
1327
- const apps = [...run.stack.apps.map((app) => {
1328
- const image = run.images[app.name];
1329
- return {
1330
- appName: app.name,
1331
- type: app.kind === "site" ? "web" : "backend",
1332
- success: !ctx.dryRun,
1333
- applicationId: `${run.stack.project}/${app.name}`,
1334
- imageRef: image?.ref ?? app.ref,
1335
- ...image?.digest ? { digest: image.digest } : {},
1336
- url: app.url
1337
- };
1338
- }), ...run.stack.workers.map((worker) => {
1339
- const image = run.images[worker.name];
1340
- return {
1341
- appName: worker.name,
1342
- type: "backend",
1343
- success: !ctx.dryRun,
1344
- applicationId: `${run.stack.project}/${worker.name}`,
1345
- imageRef: image?.ref ?? worker.ref,
1346
- ...image?.digest ? { digest: image.digest } : {}
1347
- };
1348
- })];
1349
- return {
1350
- apps,
1351
- projectId: run.stack.project,
1352
- successCount: ctx.dryRun ? 0 : apps.length,
1353
- failedCount: 0,
1354
- stage: ctx.stage,
1355
- identity: ctx.identity.key,
1356
- tag: ctx.tag,
1357
- dryRun: ctx.dryRun,
1358
- environmentId: "",
1359
- skipped: [...ctx.skipped],
1360
- urls: Object.fromEntries(run.stack.apps.map((app) => [app.name, app.url])),
1361
- changes: run.changes
1362
- };
1363
- }
1364
- function caFile(run) {
1365
- return (0, node_path.join)(run.dir, "caddy-root.crt");
1366
- }
1367
- function applied(ctx, run, change) {
1368
- const { via, ...rest } = change;
1369
- run.changes.push(rest);
1370
- ctx.emit({
1371
- type: "resource.applied",
1372
- ...rest,
1373
- via
1374
- });
1375
- }
1376
- /** Write the stack's files: owner-only, since the env files hold secrets. */
1377
- async function writeStack(root, dir, stack) {
1378
- await (0, node_fs_promises.mkdir)(dir, {
1379
- recursive: true,
1380
- mode: 448
1381
- });
1382
- await (0, node_fs_promises.chmod)(dir, 448);
1383
- const files = [];
1384
- const write = async (path, content, mode = 420) => {
1385
- await (0, node_fs_promises.mkdir)((0, node_path.dirname)(path), { recursive: true });
1386
- await (0, node_fs_promises.writeFile)(path, content, { mode });
1387
- await (0, node_fs_promises.chmod)(path, mode);
1388
- files.push(path);
1389
- };
1390
- await write((0, node_path.join)(dir, "docker-compose.yml"), composeYaml(stack));
1391
- if (stack.caddyfile !== void 0) {
1392
- await write((0, node_path.join)(dir, "Caddyfile"), stack.caddyfile);
1393
- await (0, node_fs_promises.rm)((0, node_path.join)(dir, "traefik.yml"), { force: true });
1394
- } else await (0, node_fs_promises.rm)((0, node_path.join)(dir, "Caddyfile"), { force: true });
1395
- if (stack.traefik !== void 0) await write((0, node_path.join)(dir, "traefik.yml"), stack.traefik);
1396
- if (stack.proxy === "caddy" && stack.tls) {
1397
- await (0, node_fs_promises.mkdir)((0, node_path.join)(dir, "tls"), {
1398
- recursive: true,
1399
- mode: 448
1400
- });
1401
- await write((0, node_path.join)(dir, "tls", "cert.pem"), await (0, node_fs_promises.readFile)(stack.tls.certFile, "utf-8"));
1402
- await write((0, node_path.join)(dir, "tls", "key.pem"), await (0, node_fs_promises.readFile)(stack.tls.keyFile, "utf-8"), 384);
1403
- } else await (0, node_fs_promises.rm)((0, node_path.join)(dir, "tls"), {
1404
- recursive: true,
1405
- force: true
1406
- });
1407
- for (const app of [...stack.apps, ...stack.workers]) if (app.env) await write((0, node_path.join)(dir, `${app.name}.env`), require_stack.envFile(app.env), 384);
1408
- if (stack.logs && stack.compose.services["openobserve"]?.env_file) await write((0, node_path.join)(dir, `${require_seeds.LOGS_SERVICE}.env`), require_stack.envFile(stack.logs.env), 384);
1409
- if (stack.redis && stack.compose.services["redis"]?.env_file) await write((0, node_path.join)(dir, `${require_seeds.REDIS_SERVICE}.env`), require_stack.envFile(stack.redis.env), 384);
1410
- for (const entry of await (0, node_fs_promises.readdir)(dir)) if (entry.endsWith(".credentials")) await (0, node_fs_promises.rm)((0, node_path.join)(dir, entry));
1411
- for (const [path, content] of Object.entries(stack.dockerfiles)) await write((0, node_path.join)(root, path), content);
1412
- await require_seeds.ensureDockerignore(require_seeds.findBuildRoot(root));
1413
- return files;
1414
- }
1415
- function composeYaml(stack) {
1416
- const stage = ` --stage ${stack.stage}`;
1417
- return `# Generated by gkm compose from the construct manifest — do not edit.
1418
- # The ${stack.stage} stage's APIs and sites behind ${stack.proxy === "caddy" ? "one Caddy" : `the shared Traefik edge (${require_stack.EDGE_PROJECT})`}, and its workers.
1419
- # Each backend and worker reads exactly the keys in its own env file beside
1420
- # this one.
1421
- #
1422
- # gkm compose${stage} start or update it
1423
- # gkm compose${stage} --down stop it
1424
- ${(0, yaml.stringify)(stack.compose, {
1425
- lineWidth: 0,
1426
- aliasDuplicateObjects: false
1427
- })}`;
1428
- }
1429
- function printPlan(ctx, run) {
1430
- const { stack, mode, files } = run;
1431
- ctx.logger.info(`\n🧱 ${stack.project} — stage ${stack.stage}`);
1432
- for (const app of stack.apps) ctx.logger.info(` ${app.name.padEnd(12)} ${app.url.padEnd(40)} ${mode} ${app.ref}`);
1433
- for (const worker of stack.workers) ctx.logger.info(` ${worker.name.padEnd(12)} ${`worker ${worker.id} (no route)`.padEnd(40)} ${mode} ${worker.ref}`);
1434
- if (stack.infra.length > 0) ctx.logger.info(` infrastructure: ${stack.infra.join(", ")}`);
1435
- ctx.logger.info(`\n📝 Wrote ${files.length} file(s) under ${require_stack.stackDir(stack.stage)}/`);
1436
- }
1437
- //#endregion
1438
- //#region src/target/compose/index.ts
1439
- /**
1440
- * The built-in `compose` target: a stage as one Docker Compose stack behind
1441
- * Caddy, on the machine that runs the deploy.
1442
- *
1443
- * ```bash
1444
- * gkm deploy --target compose --stage production --tag v1.4.0 # what CI pushed
1445
- * gkm deploy --target compose --stage production # built from here
1446
- * gkm deploy --target compose --stage development # the local stage
1447
- * ```
1448
- *
1449
- * - `validate`: the stack (`composeStack`, pure) and, for a tag, every image
1450
- * looked up in the registry — `ImageTagNotFound` before anything changes
1451
- * - `ready`: a deploy's own checks, after the stage's keys are — the stage's
1452
- * certificate, its server's address, and every public host's DNS
1453
- * - `plan`: the stack's files and what a run would create or reuse
1454
- * - `provision`: generated secrets kept, files written, infrastructure up,
1455
- * databases, roles, grants and migrations
1456
- * - `build`: each backend bundled on the host and every image built — or, for
1457
- * a tag, pulled
1458
- * - `release`: `up --wait --remove-orphans`, and each app's image, tag and
1459
- * digest in the stage's state
1460
- * - `verify`: each app asked through Caddy over HTTPS
1461
- *
1462
- * With `push` (`gkm compose --build --push`), run with `buildOnly`, it builds
1463
- * every image and pushes it to `deploy.registry`, and does nothing else: no
1464
- * readiness check runs, so a stage whose keys the deploy has yet to write
1465
- * still builds.
1466
- *
1467
- * `gkm compose` is this target with a few more switches (`--build`, `--pull`,
1468
- * `--down`), run through the same `deploy()`.
1469
- */
1470
- var compose_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({ composeTarget: () => composeTarget });
1471
- /** The compose target, with its Docker, bundler and probe as given. */
1472
- function composeTarget(options = {}) {
1473
- const { mode, push, pin, report, ...given } = options;
1474
- const deps = {
1475
- ...defaultDeps,
1476
- ...given
1477
- };
1478
- return require_define.defineTarget({
1479
- name: "compose",
1480
- runtime: "server",
1481
- capabilities: {
1482
- rollback: false,
1483
- migrations: "target",
1484
- images: true,
1485
- localStage: true,
1486
- selfHostedTelemetry: true
1487
- },
1488
- credentials: [],
1489
- tag: ({ cwd }) => deps.revision(cwd),
1490
- validate: (ctx) => validateCompose(ctx, deps, {
1491
- ...mode ? { mode } : {},
1492
- ...push ? { push } : {},
1493
- ...pin ? { pin } : {}
1494
- }),
1495
- ready: (ctx, run) => readyCompose(ctx, run, deps),
1496
- plan: (ctx, run) => planCompose(ctx, run),
1497
- resources: (ctx) => resourcesCompose(ctx, deps),
1498
- provision: (ctx, run) => provisionCompose(ctx, run, deps),
1499
- build: (ctx, run) => buildCompose(ctx, run, deps),
1500
- release: (ctx, run) => releaseCompose(ctx, run, deps),
1501
- verify: (ctx, run) => verifyCompose(ctx, run, deps),
1502
- result: (ctx, run) => {
1503
- report?.(run);
1504
- return composeResult(ctx, run);
1505
- }
1506
- });
1507
- }
1508
- //#endregion
1509
- Object.defineProperty(exports, "ComposePinNeedsPull", {
1510
- enumerable: true,
1511
- get: function() {
1512
- return ComposePinNeedsPull;
1513
- }
1514
- });
1515
- Object.defineProperty(exports, "ComposePushNeedsBuild", {
1516
- enumerable: true,
1517
- get: function() {
1518
- return ComposePushNeedsBuild;
1519
- }
1520
- });
1521
- Object.defineProperty(exports, "composeTarget", {
1522
- enumerable: true,
1523
- get: function() {
1524
- return composeTarget;
1525
- }
1526
- });
1527
- Object.defineProperty(exports, "compose_exports", {
1528
- enumerable: true,
1529
- get: function() {
1530
- return compose_exports;
1531
- }
1532
- });
1533
- Object.defineProperty(exports, "dockerCompose", {
1534
- enumerable: true,
1535
- get: function() {
1536
- return dockerCompose;
1537
- }
1538
- });
1539
- Object.defineProperty(exports, "edgeDir", {
1540
- enumerable: true,
1541
- get: function() {
1542
- return edgeDir;
1543
- }
1544
- });
1545
- Object.defineProperty(exports, "removeEdgeRoutes", {
1546
- enumerable: true,
1547
- get: function() {
1548
- return removeEdgeRoutes;
1549
- }
1550
- });
1551
- Object.defineProperty(exports, "stackOverrideFile", {
1552
- enumerable: true,
1553
- get: function() {
1554
- return stackOverrideFile;
1555
- }
1556
- });
1557
-
1558
- //# sourceMappingURL=compose-DMY4ouEk.cjs.map