@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,1201 +0,0 @@
1
- import { t as __exportAll } from "./rolldown-runtime-BhDjJH2R.mjs";
2
- import { t as GkmError } from "./errors-D8Gwp5kU.mjs";
3
- import { t as appKey } from "./derive-B4vDpzgP.mjs";
4
- import { B as LOGS_PORT, I as otlpTelemetryEnv, L as resolveStageTelemetry, at as certificateFor, ot as proxyFor, ut as isReservedStageKey, z as telemetryEnv } from "./workspace-BBeT6Pil.mjs";
5
- import { $n as stackLogs, Gt as WORKER_PORT, Ht as composeBuildPaths, It as appEnvKeys, N as stageProviderNotes, O as stageServiceDeclarations, Ot as bucketPolicies, Qn as logsService, Rt as networkEnv, T as externalServices, Vt as localRolePassword, Wt as TURBO_VERSION, Xn as LOGS_SERVICE, _t as REDIS_SERVICE, cn as isTelemetryKey, d as validateImageRef, dn as telemetryOf, et as workerUnits, fn as usesTelemetry, ft as LOCAL_RABBITMQ_USER, gn as portKeys, in as planFor, k as suppliedOnly, l as workerDockerfile, ln as scopeTelemetryEnv, mt as postgresSuperuser, nn as EMULATOR_ACCESS_KEY_ID, nr as isMainFrontendApp, o as appDockerfile, on as hostFor, pn as DEFAULT_IMAGES, rr as resolveHost, tn as composeFor, tr as NoDomainForStage, vt as STACK_CACHE, w as devServicesUsed, xt as stackRedis, yt as redisService, zn as imageRef, zt as workerEnvKeys } from "./seeds-BvxWUGKJ.mjs";
6
- import { i as DEFAULT_EVENTS } from "./SubscriberGenerator-CyN2VF5X.mjs";
7
- import { join } from "node:path";
8
- import { CLIENT_IP_HEADER, provideKey, provisionOrder, publicEnvFor } from "@geekmidas/manifest";
9
- import { createHash } from "node:crypto";
10
- import { stringify } from "yaml";
11
- //#region src/compose/traefik.ts
12
- /**
13
- * The server's shared edge, rendered for Traefik: what the edge itself runs
14
- * with, and what one stack registers with it.
15
- *
16
- * One Traefik per server owns 80 and 443, the ACME state and the redirect to
17
- * HTTPS. It is configured through its file provider alone: each stack writes
18
- * one file — its routers, services and middlewares, every name prefixed with
19
- * its compose project — into a directory the edge watches, and removes it
20
- * when it stops. There is no Docker socket: the edge never learns what runs
21
- * on the server except from those files.
22
- *
23
- * The edge and the stacks meet on one external Docker network. Only a
24
- * stack's public services join it, each under an alias prefixed with its
25
- * project (`<project>-api`), because the network is shared: a bare service
26
- * name there would be answered by every stack's `api`.
27
- *
28
- * Pure: the documents are data, and the edge's side effects are `edge.ts`'s.
29
- */
30
- /**
31
- * The image, pinned: the edge is every stack's way in, so a new version is a
32
- * change to this line, not whatever `latest` is on the day a server starts.
33
- */
34
- const TRAEFIK_IMAGE = "traefik:v3.7.13";
35
- /** The edge's compose project, one per server. */
36
- const EDGE_PROJECT = "gkm-edge";
37
- /** The external network the edge and every stack's public services share. */
38
- const EDGE_NETWORK = "gkm-edge";
39
- /** The edge's compose service. */
40
- const EDGE_SERVICE = "traefik";
41
- /** Inside the edge's container: what it watches, and where certificates are. */
42
- const EDGE_DYNAMIC_DIR = "/etc/gkm-edge/dynamic";
43
- const EDGE_CERTS_DIR = "/etc/gkm-edge/certs";
44
- /** The certificate resolver every deployed route without its own certificate uses. */
45
- const ACME_RESOLVER = "letsencrypt";
46
- /** The internal entrypoint the edge's health check asks; never published. */
47
- const PING_PORT = 8082;
48
- /** A stack's service, as the edge — and the stack itself — reach it. */
49
- function edgeAlias(project, service) {
50
- return `${project}-${service}`;
51
- }
52
- /** A stack's certificate, as the edge's container reads it. */
53
- function edgeCertificatePaths(project) {
54
- return {
55
- certFile: `${EDGE_CERTS_DIR}/${project}.crt`,
56
- keyFile: `${EDGE_CERTS_DIR}/${project}.key`
57
- };
58
- }
59
- /**
60
- * The edge's static configuration: the entrypoints, the redirect, ACME, and
61
- * the directory it watches. No API and no dashboard — nothing about the edge
62
- * is reachable but the routes stacks register.
63
- */
64
- function traefikStatic(ports) {
65
- return {
66
- global: {
67
- checkNewVersion: false,
68
- sendAnonymousUsage: false
69
- },
70
- entryPoints: {
71
- web: {
72
- address: ":80",
73
- http: { redirections: { entryPoint: {
74
- to: ports.https === 443 ? "websecure" : `:${ports.https}`,
75
- scheme: "https",
76
- permanent: true
77
- } } }
78
- },
79
- websecure: { address: ":443" },
80
- ping: { address: `:${PING_PORT}` }
81
- },
82
- ping: { entryPoint: "ping" },
83
- providers: { file: {
84
- directory: EDGE_DYNAMIC_DIR,
85
- watch: true
86
- } },
87
- certificatesResolvers: { [ACME_RESOLVER]: { acme: {
88
- storage: "/acme/acme.json",
89
- httpChallenge: { entryPoint: "web" }
90
- } } },
91
- log: { level: "INFO" }
92
- };
93
- }
94
- /** The edge's compose document. */
95
- function edgeCompose(options) {
96
- const { dir, ports } = options;
97
- return {
98
- name: EDGE_PROJECT,
99
- services: { [EDGE_SERVICE]: {
100
- image: TRAEFIK_IMAGE,
101
- restart: "unless-stopped",
102
- ports: [`${ports.https}:443`, `${ports.http}:80`],
103
- volumes: [
104
- `${dir}/traefik.yml:/etc/traefik/traefik.yml:ro`,
105
- `${dir}/dynamic:${EDGE_DYNAMIC_DIR}:ro`,
106
- `${dir}/certs:${EDGE_CERTS_DIR}:ro`,
107
- "acme:/acme"
108
- ],
109
- labels: { "dev.geekmidas.edge.config": createHash("sha256").update(options.staticConfig).digest("hex").slice(0, 16) },
110
- networks: ["edge"],
111
- healthcheck: {
112
- test: [
113
- "CMD",
114
- "traefik",
115
- "healthcheck",
116
- "--ping"
117
- ],
118
- interval: "10s",
119
- timeout: "5s",
120
- retries: 5
121
- },
122
- logging: options.logging
123
- } },
124
- volumes: { acme: {} },
125
- networks: { edge: {
126
- name: EDGE_NETWORK,
127
- external: true
128
- } }
129
- };
130
- }
131
- /** A stack's dynamic configuration, as an object. */
132
- function traefikDynamic(routes, options) {
133
- const { project, tls } = options;
134
- const name = (route, suffix) => `${project}-${route.name}${suffix ? `-${suffix}` : ""}`;
135
- const url = ({ service, port }) => `http://${edgeAlias(project, service)}:${port}`;
136
- const routers = {};
137
- const services = {};
138
- const middlewares = {};
139
- const strip = `${project}-strip-gkm-headers`;
140
- if (routes.length > 0) middlewares[strip] = { headers: { customRequestHeaders: { [CLIENT_IP_HEADER]: "" } } };
141
- for (const route of routes) {
142
- const chain = [strip];
143
- if (route.allow) {
144
- middlewares[name(route, "allow")] = { ipAllowList: { sourceRange: [...route.allow] } };
145
- chain.push(name(route, "allow"));
146
- }
147
- if (route.prefix) {
148
- middlewares[name(route, "prefix")] = { addPrefix: { prefix: route.prefix } };
149
- chain.push(name(route, "prefix"));
150
- }
151
- routers[name(route)] = {
152
- rule: `Host(\`${route.host}\`)`,
153
- entryPoints: ["websecure"],
154
- service: name(route),
155
- middlewares: chain,
156
- tls: tls.kind === "acme" ? { certResolver: ACME_RESOLVER } : {}
157
- };
158
- services[name(route)] = { loadBalancer: {
159
- servers: route.upstreams.map((upstream) => ({ url: url(upstream) })),
160
- ...route.prefix ? { passHostHeader: false } : {},
161
- ...route.streaming ? { responseForwarding: { flushInterval: "-1ms" } } : {},
162
- ...route.health ? { healthCheck: {
163
- path: route.health,
164
- interval: "5s",
165
- timeout: "3s"
166
- } } : {}
167
- } };
168
- }
169
- return {
170
- http: {
171
- routers,
172
- services,
173
- ...Object.keys(middlewares).length > 0 ? { middlewares } : {}
174
- },
175
- ...tls.kind === "files" ? { tls: { certificates: [{
176
- certFile: tls.certFile,
177
- keyFile: tls.keyFile
178
- }] } } : {}
179
- };
180
- }
181
- /** A stack's dynamic configuration file, as written to the edge's directory. */
182
- function traefikDynamicFile(routes, options) {
183
- return `# Generated by gkm compose for ${options.project} — do not edit.
184
- # The shared edge (${EDGE_PROJECT}) reads every file in this directory; this
185
- # one is removed by gkm compose --down.
186
- ${yaml(traefikDynamic(routes, options))}`;
187
- }
188
- /** The edge's static configuration file. */
189
- function traefikStaticFile(ports) {
190
- return `# Generated by gkm compose — the server's shared edge. Do not edit.
191
- ${yaml(traefikStatic(ports))}`;
192
- }
193
- /** The edge's compose file. */
194
- function edgeComposeFile(options) {
195
- return `# Generated by gkm compose — the server's shared edge. Do not edit.
196
- #
197
- # Started by the first stack that runs with proxy: 'traefik', and left
198
- # running by gkm compose --down. Stop it, once nothing uses it, with:
199
- #
200
- # docker compose -p ${EDGE_PROJECT} -f ${options.dir}/docker-compose.yml down
201
- ${yaml(edgeCompose(options))}`;
202
- }
203
- function yaml(value) {
204
- return stringify(value, {
205
- lineWidth: 0,
206
- aliasDuplicateObjects: false
207
- });
208
- }
209
- //#endregion
210
- //#region src/compose/images.ts
211
- /**
212
- * Which image each app runs — and, for a tag, whether every one exists.
213
- *
214
- * A tag is a release's identity: `--tag=v1.4.0` means the images CI pushed as
215
- * v1.4.0, and nothing else. So a stack is never half rolled out: every app's
216
- * image is asked of the registry before anything is pulled or the running
217
- * stack is touched, and one missing image stops the whole run naming every
218
- * one that is missing. Nor is a missing image quietly built instead — a build
219
- * of whatever is checked out here is not the release the tag names.
220
- */
221
- /**
222
- * The tag a site's image carries for a release.
223
- *
224
- * A site's public URLs are inlined when it is built, so one bundle serves one
225
- * stage: `v1.4.0-production` and `v1.4.0-staging` are two images of the same
226
- * commit. A backend reads its URLs at runtime, so one image serves every stage
227
- * and its tag is the release's alone.
228
- */
229
- function siteTag(tag, stage) {
230
- return `${tag}-${stage}`;
231
- }
232
- /** Images a tag names that the registry does not have. */
233
- var ImageTagNotFound = class extends GkmError {
234
- tag;
235
- refs;
236
- constructor(tag, refs) {
237
- super(`The registry has no image for ${refs.length === 1 ? "this app" : `${refs.length} apps`} at '${tag}':\n` + refs.map((ref) => ` - ${ref}`).join("\n") + `\nNothing was pulled or started. Push '${tag}' for every app from CI, pick a tag that exists, or run without --tag to build from this checkout.`);
238
- this.tag = tag;
239
- this.refs = refs;
240
- this.name = "ImageTagNotFound";
241
- }
242
- };
243
- /** The registry did not answer, or did not let this machine in. */
244
- var RegistryUnreachable = class extends GkmError {
245
- ref;
246
- detail;
247
- constructor(ref, detail) {
248
- super(`Could not ask the registry about ${ref}: ${detail || "no answer"}\nNothing was pulled or started. Check the registry is reachable and that this machine is logged in to it (\`docker login <registry>\`).`);
249
- this.ref = ref;
250
- this.detail = detail;
251
- this.name = "RegistryUnreachable";
252
- }
253
- };
254
- /**
255
- * Assert every image exists, before anything else happens.
256
- *
257
- * Asked all at once and reported all at once: a release missing two images is
258
- * one problem with two names, and finding the second only after fixing the
259
- * first is a slower way to learn the same thing.
260
- *
261
- * @throws {RegistryUnreachable} when the registry cannot say
262
- * @throws {ImageTagNotFound} listing every missing image
263
- */
264
- async function assertImagesExist(docker, tag, images) {
265
- const lookups = await Promise.all(images.map((image) => docker.lookup(image.ref)));
266
- const unreachable = lookups.find((lookup) => lookup.status === "unreachable");
267
- if (unreachable?.status === "unreachable") throw new RegistryUnreachable(unreachable.ref, unreachable.detail);
268
- const missing = lookups.filter((lookup) => lookup.status === "missing").map((lookup) => lookup.ref);
269
- if (missing.length > 0) throw new ImageTagNotFound(tag, missing);
270
- }
271
- /**
272
- * A push or a pull with no `deploy.registry` to push to or pull from.
273
- *
274
- * Without one an image is named `<namespace>/<project>-<app>:<tag>`, which
275
- * Docker resolves to Docker Hub — so a release would be pushed to, or pulled
276
- * from, somebody else's account. It is never a fallback.
277
- */
278
- var RegistryRequired = class extends GkmError {
279
- operation;
280
- ref;
281
- constructor(operation, ref) {
282
- super(`${operation === "push" ? "Pushing" : "Pulling"} images needs a registry, and gkm.config.ts sets no deploy.registry: '${ref}' would be ${operation === "push" ? "pushed to" : "pulled from"} Docker Hub. Set deploy.registry (e.g. registry: 'ghcr.io/acme') in gkm.config.ts.` + (operation === "pull" ? " To run images built here instead, pass --build." : ""));
283
- this.operation = operation;
284
- this.ref = ref;
285
- this.name = "RegistryRequired";
286
- }
287
- };
288
- /** An image pinned to its digest: `<ref>@sha256:…`. */
289
- function pinnedRef(ref, digest) {
290
- return `${ref.split("@")[0]}@${digest}`;
291
- }
292
- /** A digests file with no entry for an app the stack runs. */
293
- var ImageDigestMissing = class extends GkmError {
294
- file;
295
- apps;
296
- constructor(file, apps) {
297
- super(`${file} pins no digest for ${apps.join(", ")}. Use the file the same release's \`gkm compose --build --push --digests-file\` wrote, or run without --digests-file to pull by tag.`);
298
- this.file = file;
299
- this.apps = apps;
300
- this.name = "ImageDigestMissing";
301
- }
302
- };
303
- /** A digests file entry that is not this app's image. */
304
- var ImageDigestMismatch = class extends GkmError {
305
- app;
306
- expected;
307
- found;
308
- constructor(app, expected, found) {
309
- super(`The digests file pins '${app}' to ${found}, but this release runs ${expected}. It was written for another registry, project, stage or tag; use the file this stage's push of this tag wrote.`);
310
- this.app = app;
311
- this.expected = expected;
312
- this.found = found;
313
- this.name = "ImageDigestMismatch";
314
- }
315
- };
316
- /** A digests file that is not a JSON object of app to pinned ref. */
317
- var ImageDigestsInvalid = class extends GkmError {
318
- file;
319
- detail;
320
- constructor(file, detail) {
321
- super(`${file} is not a digests file (${detail}). It should be the JSON object \`gkm compose --build --push --digests-file\` writes: { "api": "<registry>/<image>:<tag>@sha256:…" }.`);
322
- this.file = file;
323
- this.detail = detail;
324
- this.name = "ImageDigestsInvalid";
325
- }
326
- };
327
- /** Parse a digests file's content, checking its shape. */
328
- function parseDigests(file, content) {
329
- let parsed;
330
- try {
331
- parsed = JSON.parse(content);
332
- } catch (error) {
333
- throw new ImageDigestsInvalid(file, error instanceof Error ? error.message : String(error));
334
- }
335
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new ImageDigestsInvalid(file, "not a JSON object");
336
- for (const [app, value] of Object.entries(parsed)) if (typeof value !== "string" || !/@sha256:[0-9a-f]{64}$/.test(value)) throw new ImageDigestsInvalid(file, `'${app}' is not an <image>:<tag>@sha256:… ref`);
337
- return parsed;
338
- }
339
- /**
340
- * Each image pinned to the digest the file names for it.
341
- *
342
- * @throws {ImageDigestMissing} naming every app the file has no entry for
343
- * @throws {ImageDigestMismatch} when an entry is another image or tag
344
- */
345
- function pinImages(file, digests, images) {
346
- const missing = images.filter((image) => !digests[image.app]).map((image) => image.app);
347
- if (missing.length > 0) throw new ImageDigestMissing(file, missing);
348
- return images.map((image) => {
349
- const pinned = digests[image.app];
350
- if (pinned.split("@")[0] !== image.ref) throw new ImageDigestMismatch(image.app, image.ref, pinned);
351
- return {
352
- ...image,
353
- ref: pinned
354
- };
355
- });
356
- }
357
- //#endregion
358
- //#region src/compose/caddyfile.ts
359
- /**
360
- * The stack's own edge, rendered for Caddy: one site block per route.
361
- *
362
- * Deployed, each host is the stage's real domain and Caddy gets it a
363
- * certificate from Let's Encrypt on first request — the same automatic HTTPS
364
- * it does anywhere, with nothing to configure but the name — or serves the
365
- * stage's own certificate (`deploy.compose.tls`). Locally the hosts are
366
- * `*.localhost` and the certificates come from Caddy's own CA, as the `gkm
367
- * dev` edge does, so a local stack is `https://` like a deployed one.
368
- *
369
- * Pure: the routes are data (`routes.ts`), and rendering them is one string.
370
- */
371
- /** Where Caddy reads a stage's own certificate, inside its container. */
372
- const CADDY_TLS_DIR = "/etc/caddy/tls";
373
- /** An upstream on the stack's own network: its service name. */
374
- function address({ service, port }) {
375
- return `${service}:${port}`;
376
- }
377
- /** The Caddyfile for a stack's edge. */
378
- function edgeCaddyfile(routes, options) {
379
- const { tls } = options;
380
- const global = tls.kind === "internal" ? `{
381
- # No ACME and no public DNS: the CA is Caddy's own and the names resolve
382
- # to this machine.
383
- local_certs
384
- auto_https disable_redirects
385
- }` : options.email ? `{
386
- email ${options.email}
387
- }` : "";
388
- const certificate = tls.kind === "internal" ? `
389
- tls internal
390
- ` : tls.kind === "files" ? `
391
- tls ${tls.certFile} ${tls.keyFile}
392
- ` : "";
393
- const blocks = routes.map(({ host, upstreams, prefix, allow, streaming }) => `https://${host} {${certificate}
394
- # gkm's own header, which only a service of this stack may send: the auth
395
- # server rate-limits by the client address it carries.
396
- request_header -${CLIENT_IP_HEADER}
397
- ${allow ? `
398
- # Only these addresses — the peer Caddy sees, never a header a client
399
- # could set. Everything else is refused before it reaches the upstream.
400
- @denied not remote_ip ${allow.join(" ")}
401
- respond @denied 403
402
- ` : ""}${prefix ? `
403
- # A bucket, served at a host of its own: the bucket is a prefix on MinIO,
404
- # which routes and signs on the Host header — so it is sent the upstream's.
405
- reverse_proxy ${upstreams.map(address).join(" ")} {
406
- rewrite ${prefix}{uri}
407
- header_up Host {upstream_hostport}
408
- }` : `
409
- # Host and X-Forwarded-* are passed through as Caddy does by default, so
410
- # an app builds redirects, cookies and links on the address a caller used.
411
- reverse_proxy ${upstreams.map(address).join(" ")}${streaming ? ` {
412
- # Never buffer: a streamed response — server-sent events, a Next.js
413
- # RSC payload — reaches the client as each chunk is written.
414
- flush_interval -1
415
- }` : ""}`}
416
- }`);
417
- return `${HEADER}${global ? `${global}\n\n` : ""}${blocks.join("\n\n")}\n`;
418
- }
419
- const HEADER = `# Generated by gkm compose from the construct manifest — do not edit.
420
- #
421
- # One host per API and site, each in front of its own container, one per
422
- # file server in front of the stack's MinIO, and — when its logs are served
423
- # publicly — one for OpenObserve, to the addresses it allows.
424
- `;
425
- //#endregion
426
- //#region src/compose/stack.ts
427
- /**
428
- * The stack a workspace runs as for one stage — pure.
429
- *
430
- * Everything `gkm compose` writes is decided here, as data: the compose file,
431
- * the Caddyfile, one env file per backend, and the image each app runs. The
432
- * command around it reads the workspace, asks the registry, writes the files
433
- * and starts Docker; none of those decide anything, so all of it can be
434
- * asserted without a daemon.
435
- *
436
- * The values come from the same derivations every other target uses. The
437
- * plan is reconcile's `planFor`, the keys an app reads are `appEnvKeys`, and
438
- * every URL is `envFor`'s — asked twice, once with each app at its public
439
- * address and once on the compose network, because a browser and a sibling
440
- * service reach the same API by different names.
441
- */
442
- var stack_exports = /* @__PURE__ */ __exportAll({
443
- EDGE_PORT_ENV: () => EDGE_PORT_ENV,
444
- EnvValueMultiline: () => EnvValueMultiline,
445
- LOGS_PORT_ENV: () => LOGS_PORT_ENV,
446
- LOG_ROTATION: () => LOG_ROTATION,
447
- LocalCredentialsMissing: () => LocalCredentialsMissing,
448
- NothingToCompose: () => NothingToCompose,
449
- StageSecretMissing: () => StageSecretMissing,
450
- StageSeedMissing: () => StageSeedMissing,
451
- composeProject: () => composeProject,
452
- composeStack: () => composeStack,
453
- derivedCredentials: () => derivedCredentials,
454
- envFile: () => envFile,
455
- stackDir: () => stackDir
456
- });
457
- /** Where a stack's files are written, relative to the workspace root. */
458
- function stackDir(stage) {
459
- return `.gkm/compose/${stage}`;
460
- }
461
- /**
462
- * The compose project a stage's stack runs as.
463
- *
464
- * The deploy identity's scope, so two workspaces with one name in different
465
- * namespaces never share containers or volumes — and the stage, so a
466
- * machine can run two stages side by side.
467
- */
468
- function composeProject(identity) {
469
- return `${identity.scope}-${identity.stage}`;
470
- }
471
- /** The ports the edge is published on, and the variables that move them. */
472
- const EDGE_PORT_ENV = {
473
- https: "GKM_COMPOSE_HTTPS_PORT",
474
- http: "GKM_COMPOSE_HTTP_PORT"
475
- };
476
- /**
477
- * The variable that moves the local stage's OpenObserve off its loopback
478
- * port, 5080 — which the local stage takes from nowhere else, since it
479
- * ignores `deploy.telemetry`. Another stack, or another tool, may hold 5080.
480
- */
481
- const LOGS_PORT_ENV = "GKM_COMPOSE_LOGS_PORT";
482
- /**
483
- * Containers this target does not run.
484
- *
485
- * The AWS emulator never applies: a stack is a server target, whose events
486
- * are pg-boss. Reconcile's Caddy fronts `gkm dev`'s host processes; the stack
487
- * brings its own edge.
488
- *
489
- * MinIO and Mailpit run on the local stage always, and on a deployed stage
490
- * only where `--allow-dev-services` allowed them and its secrets configure no
491
- * real bucket or mail server — see `devServices.ts`.
492
- */
493
- const NOT_RUN = {
494
- localstack: true,
495
- caddy: true
496
- };
497
- /** The kinds a deployed stage takes from its secrets, or a dev service. */
498
- const SERVICE_KINDS = {
499
- email: "email",
500
- objects: "objects",
501
- "file-server": "file-server"
502
- };
503
- /** The region MinIO is addressed with. It has no regions; the SDK wants one. */
504
- const MINIO_REGION = "us-east-1";
505
- /** The S3 client's credentials, read beside a bucket's URL when it has them. */
506
- const STORAGE_KEYS = [
507
- "AWS_ACCESS_KEY_ID",
508
- "AWS_SECRET_ACCESS_KEY",
509
- "AWS_REGION"
510
- ];
511
- /**
512
- * Every service's Docker logs, rotated. The json-file driver keeps a
513
- * container's output in one file that grows until the disk is full — on a
514
- * small server, in weeks. Three files of 10 MB each per container is enough
515
- * to see what happened and never fills anything. A project's own override
516
- * file that sets `logging` wins over it.
517
- */
518
- const LOG_ROTATION = {
519
- driver: "json-file",
520
- options: {
521
- "max-size": "10m",
522
- "max-file": "3"
523
- }
524
- };
525
- /** The local stage's stack, without the logins `gkm dev` generates. */
526
- var LocalCredentialsMissing = class extends GkmError {
527
- stage;
528
- constructor(stage) {
529
- super(`The local stage '${stage}' runs with this machine's generated logins, and none were passed. Load them with loadLocalCredentials(workspace) and pass them as localCredentials.`);
530
- this.stage = stage;
531
- this.name = "LocalCredentialsMissing";
532
- }
533
- };
534
- /**
535
- * A deployed stage's container logins, derived from its seed like its role
536
- * passwords — so every deploy of the stage computes the same ones, and no one
537
- * reading the repo can.
538
- */
539
- function derivedCredentials(project, plan, seed) {
540
- const derive = (purpose) => localRolePassword(project, plan, purpose, seed);
541
- return {
542
- postgres: {
543
- user: postgresSuperuser(project),
544
- password: derive("master")
545
- },
546
- minio: {
547
- user: `${project}-minio`,
548
- password: derive("minio")
549
- },
550
- rabbitmq: {
551
- user: LOCAL_RABBITMQ_USER,
552
- password: derive("rabbitmq")
553
- },
554
- redis: { password: derive("redis") },
555
- cacheToken: derive("cache-token"),
556
- emulator: {
557
- accessKeyId: EMULATOR_ACCESS_KEY_ID,
558
- secretAccessKey: derive("emulator")
559
- },
560
- logs: {
561
- email: `admin@${project}`,
562
- password: derive("logs")
563
- }
564
- };
565
- }
566
- /** A value only the stage's secrets can hold, and they do not. */
567
- var StageSecretMissing = class extends GkmError {
568
- app;
569
- key;
570
- stage;
571
- constructor(app, key, stage) {
572
- super(`'${app}' reads ${key}, and the stage '${stage}' has none. Nothing in the stack can derive it; set it in the stage's secrets: gkm secrets:set ${key} '…' --stage ${stage}`);
573
- this.app = app;
574
- this.key = key;
575
- this.stage = stage;
576
- this.name = "StageSecretMissing";
577
- }
578
- };
579
- /** A deployed stage's secrets with no seed to derive its passwords from. */
580
- var StageSeedMissing = class extends GkmError {
581
- stage;
582
- constructor(stage) {
583
- super(`The stage '${stage}' has no seed in its secrets, and a deployed stage's database passwords are derived from it. Run gkm compose --stage ${stage} without --dry-run once, or gkm deploy, to generate it.`);
584
- this.stage = stage;
585
- this.name = "StageSeedMissing";
586
- }
587
- };
588
- /** A workspace with no API and no site to run. */
589
- var NothingToCompose = class extends GkmError {
590
- root;
591
- constructor(root) {
592
- super(`${root} declares no RestApi, no site and no Worker with work to do, so a stack would run nothing. Mobile apps ship through their own toolchain.`);
593
- this.root = root;
594
- this.name = "NothingToCompose";
595
- }
596
- };
597
- /** A value that cannot be written to an env file a line at a time. */
598
- var EnvValueMultiline = class extends GkmError {
599
- app;
600
- key;
601
- constructor(app, key) {
602
- super(`${key} for '${app}' spans more than one line, and an env file holds one value per line. Store it on one line — base64, or JSON — and decode it where it is read.`);
603
- this.app = app;
604
- this.key = key;
605
- this.name = "EnvValueMultiline";
606
- }
607
- };
608
- /** The services a stack's apps wait on and the edge waits on in turn. */
609
- const HEALTHY = { condition: "service_healthy" };
610
- /**
611
- * An app's health check: its own server, asked on 127.0.0.1 — not
612
- * `localhost`, which resolves to `::1` first in an Alpine container while a
613
- * server may listen on IPv4 alone.
614
- */
615
- function probe(port, path) {
616
- return {
617
- test: [
618
- "CMD",
619
- "wget",
620
- "-q",
621
- "-O",
622
- "/dev/null",
623
- `http://127.0.0.1:${port}${path}`
624
- ],
625
- interval: "5s",
626
- timeout: "3s",
627
- retries: 20,
628
- start_period: "10s"
629
- };
630
- }
631
- /** The stack for one stage. */
632
- function composeStack(input) {
633
- const { workspace, manifest, stage, identity } = input;
634
- const local = stage === workspace.stages.local;
635
- const project = composeProject(identity);
636
- const custom = input.secrets?.custom ?? {};
637
- const https = input.ports?.https ?? 443;
638
- const http = input.ports?.http ?? 80;
639
- const proxy = proxyFor(workspace.deploy?.compose, stage, local);
640
- const certificate = local ? void 0 : certificateFor(workspace.deploy?.compose, stage, workspace.root);
641
- const plan = planFor(manifest, stage, provisionOrder(manifest), {
642
- localStage: workspace.stages.local,
643
- events: DEFAULT_EVENTS.server,
644
- cache: STACK_CACHE,
645
- edge: false
646
- });
647
- const byId = new Map(plan.resources.map((r) => [r.id, r]));
648
- if (local && !input.localCredentials) throw new LocalCredentialsMissing(stage);
649
- const seed = local ? input.localCredentials.seed : input.secrets?.seed;
650
- if (!seed) throw new StageSeedMissing(stage);
651
- const containers = local ? input.localCredentials : derivedCredentials(workspace.name, plan, seed);
652
- const credential = {
653
- containers: {
654
- ...containers,
655
- minio: {
656
- user: custom.AWS_ACCESS_KEY_ID ?? containers.minio.user,
657
- password: custom.AWS_SECRET_ACCESS_KEY ?? containers.minio.password
658
- }
659
- },
660
- seed
661
- };
662
- const apps = stackApps(workspace, manifest, plan, {
663
- local,
664
- stage,
665
- https,
666
- identity,
667
- images: input.images
668
- });
669
- const workers = stackWorkers(workerUnits(workspace, manifest, input.background ?? {}), {
670
- stage,
671
- identity,
672
- images: input.images
673
- });
674
- if (apps.length === 0 && workers.length === 0) throw new NothingToCompose(workspace.root);
675
- const owners = /* @__PURE__ */ new Map();
676
- for (const resource of plan.resources) {
677
- owners.set(resource.envKey, resource);
678
- if (resource.kind === "email") {
679
- owners.set(provideKey(resource.id, "from"), resource);
680
- owners.set(provideKey(resource.id, "inboxUrl"), resource);
681
- }
682
- if (resource.kind === "external-api") owners.set(provideKey(resource.id, "credentials"), resource);
683
- }
684
- const domain = workspace.domains?.[stage];
685
- const telemetry = resolveStageTelemetry({
686
- ...workspace.deploy?.telemetry ? { telemetry: workspace.deploy.telemetry } : {},
687
- stage,
688
- local,
689
- target: "compose",
690
- runtime: "server",
691
- selfHosted: true,
692
- used: usesTelemetry(manifest)
693
- });
694
- const logsBase = telemetry?.provider === "self-hosted" ? stackLogs({
695
- config: local && input.ports?.logs ? {
696
- ...telemetry.selfHosted,
697
- port: input.ports.logs
698
- } : telemetry.selfHosted,
699
- stage,
700
- local,
701
- project: workspace.name,
702
- ...domain ? { domain } : {},
703
- custom,
704
- https,
705
- ...local ? { localLogin: input.localCredentials.logs } : {}
706
- }) : void 0;
707
- const services = local ? {
708
- missing: [],
709
- minio: plan.resources.filter((r) => r.kind === "objects").map((r) => r.id),
710
- mailpit: plan.resources.filter((r) => r.kind === "email").map((r) => r.id)
711
- } : externalServices({
712
- stage,
713
- declarations: stageServiceDeclarations({
714
- manifest,
715
- ...input.runnables ? { runnables: input.runnables } : {}
716
- }),
717
- supplied: custom,
718
- allow: input.allowDevServices === true,
719
- ...domain ? { domain } : {},
720
- providers: stageProviderNotes(workspace, stage)
721
- });
722
- const redis = stackRedis({
723
- plan,
724
- stage,
725
- local,
726
- custom,
727
- ...local ? { localPassword: input.localCredentials.redis.password } : {}
728
- });
729
- const infra = [...plan.containers.filter((c) => !NOT_RUN[c]).filter((c) => c !== LOGS_SERVICE).filter((c) => c !== "minio" || services.minio.length > 0).filter((c) => c !== "mailpit" || services.mailpit.length > 0).filter((c) => c !== "redis" || redis !== void 0), ...logsBase ? [LOGS_SERVICE] : []].sort();
730
- const storage = infra.includes("minio") ? (() => {
731
- const buckets = services.minio.map((id) => byId.get(id)?.name).filter((name) => Boolean(name)).sort();
732
- return {
733
- ...credential.containers.minio,
734
- buckets,
735
- policies: bucketPolicies(plan).filter((p) => buckets.includes(p.bucket))
736
- };
737
- })() : void 0;
738
- const fileServers = /* @__PURE__ */ new Map();
739
- if (storage) for (const resource of plan.resources) {
740
- if (resource.kind !== "file-server" || !resource.of) continue;
741
- if (!services.minio.includes(resource.of)) continue;
742
- if (custom[resource.envKey] !== void 0) continue;
743
- const bucket = byId.get(resource.of);
744
- if (!bucket) continue;
745
- let host;
746
- if (local) host = hostFor(resource, workspace.name);
747
- else {
748
- if (!domain) throw new NoDomainForStage(stage);
749
- host = `${resource.subdomain ?? appKey(resource.id)}.${domain}`;
750
- }
751
- fileServers.set(resource.id, {
752
- url: `https://${host}${local && https !== 443 ? `:${https}` : ""}`,
753
- route: {
754
- name: `files-${appKey(resource.id)}`,
755
- host,
756
- upstreams: [{
757
- service: "minio",
758
- port: 9e3
759
- }],
760
- streaming: false,
761
- prefix: `/${bucket.name}`
762
- }
763
- });
764
- }
765
- const edgeServices = new Set(proxy === "traefik" ? [
766
- ...apps.map((app) => app.name),
767
- ...fileServers.size > 0 ? ["minio"] : [],
768
- ...logsBase?.host && logsBase.public ? [LOGS_SERVICE] : []
769
- ] : []);
770
- const hostOf = (service) => edgeServices.has(service) ? edgeAlias(project, service) : service;
771
- const logs = logsBase && {
772
- ...logsBase,
773
- appEnv: {
774
- ...logsBase.appEnv,
775
- OTEL_EXPORTER_OTLP_ENDPOINT: logsBase.appEnv.OTEL_EXPORTER_OTLP_ENDPOINT.replace(`//openobserve:`, `//${hostOf("openobserve")}:`)
776
- }
777
- };
778
- const derivation = {
779
- project: workspace.name,
780
- credentials: credential.containers,
781
- seed: credential.seed,
782
- ...!local && domain ? { mailFrom: `noreply@${domain}` } : {}
783
- };
784
- const outside = networkEnv(plan, {
785
- ...derivation,
786
- addresses: Object.fromEntries(apps.map((app) => [app.id, app.url]))
787
- }, hostOf);
788
- const inside = networkEnv(plan, {
789
- ...derivation,
790
- addresses: Object.fromEntries(apps.map((app) => [app.id, `http://${hostOf(app.name)}:${app.port}`]))
791
- }, hostOf);
792
- const surfaceUrls = new Map(plan.resources.filter((r) => r.kind === "rest-api" || r.kind === "site").map((r) => [r.envKey, r.id]));
793
- const appById = new Map(apps.map((app) => [app.id, app]));
794
- /**
795
- * What a key resolves to for one app, from the stage's secrets where only
796
- * they can hold it, and the derivations otherwise.
797
- */
798
- const valueFor = (app, key, owner) => {
799
- if (isReservedStageKey(key)) return void 0;
800
- if (owner && SERVICE_KINDS[owner.kind]) {
801
- if (custom[key] !== void 0) return custom[key];
802
- if (owner.kind === "file-server") return fileServers.get(owner.id)?.url;
803
- return outside[key];
804
- }
805
- if (owner && suppliedOnly(owner.kind, owner.id, key, local)) {
806
- const value = custom[key];
807
- if (value === void 0) throw new StageSecretMissing(app.name, key, stage);
808
- return value;
809
- }
810
- if (custom[key] !== void 0) return custom[key];
811
- if (owner?.kind === "cache") return redis?.urls[key];
812
- const surface = surfaceUrls.get(key);
813
- if (app.kind === "rest-api" && surface && surface !== app.id) return inside[key];
814
- return outside[key];
815
- };
816
- const telemetryValues = logs ? telemetryEnv({
817
- endpoint: logs.appEnv.OTEL_EXPORTER_OTLP_ENDPOINT,
818
- headers: logs.appEnv.OTEL_EXPORTER_OTLP_HEADERS
819
- }, telemetry?.sampleRate ?? 1) : telemetry?.provider === "otlp" ? otlpTelemetryEnv(telemetry) : void 0;
820
- /** Each key one backend process reads, resolved for this stage. */
821
- const valuesFor = (app, keys) => {
822
- const values = {};
823
- for (const key of [...keys].sort()) {
824
- if (isTelemetryKey(key)) continue;
825
- const owner = owners.get(key) ?? storageOwner(key, plan);
826
- if (!local && owner?.kind === "email" && key.endsWith("_INBOX_URL")) continue;
827
- if (STORAGE_KEYS.includes(key)) {
828
- const value = custom[key] ?? storageValue(key, storage);
829
- if (value !== void 0) values[key] = value;
830
- continue;
831
- }
832
- const value = valueFor(app, key, owner);
833
- if (value !== void 0) values[key] = value;
834
- }
835
- return values;
836
- };
837
- for (const app of apps) {
838
- if (app.kind === "site") {
839
- const site = manifest[app.id];
840
- if (site?.kind !== "site") continue;
841
- const args = {};
842
- for (const [key, source] of Object.entries(publicEnvFor(site, manifest))) {
843
- const value = valueFor(app, source, owners.get(source));
844
- if (value !== void 0) args[key] = value;
845
- }
846
- if (app.build && Object.keys(args).length > 0) app.build.args = args;
847
- continue;
848
- }
849
- if (input.buildOnly) continue;
850
- const keys = appEnvKeys(manifest, app.name, input.runnables) ?? /* @__PURE__ */ new Set();
851
- const values = valuesFor(app, keys);
852
- const trusted = provideKey(app.id, "trustedOrigins");
853
- if (keys.has(trusted)) {
854
- const internal = (byId.get(app.id)?.callers ?? []).map((caller) => appById.get(caller)).filter((caller) => caller?.kind === "rest-api").map((caller) => `http://${hostOf(caller.name)}:${caller.port}`);
855
- const origins = [...(values[trusted] ?? "").split(",").filter(Boolean), ...internal];
856
- values[trusted] = [...new Set(origins)].join(",");
857
- }
858
- app.env = {
859
- ...scopeTelemetryEnv(values, {
860
- uses: telemetryOf(manifest, app.id) !== void 0,
861
- serviceName: app.name,
862
- ...telemetryValues ? { telemetry: telemetryValues } : {}
863
- }),
864
- NODE_ENV: "production",
865
- PORT: String(app.port),
866
- STAGE: stage
867
- };
868
- for (const [key, value] of Object.entries(app.env)) if (/[\r\n]/.test(value)) throw new EnvValueMultiline(app.name, key);
869
- }
870
- for (const worker of input.buildOnly ? [] : workers) {
871
- const keys = workerEnvKeys(manifest, worker.id, input.runnables) ?? /* @__PURE__ */ new Set();
872
- worker.env = {
873
- ...scopeTelemetryEnv(valuesFor({
874
- name: worker.name,
875
- id: worker.id,
876
- kind: "rest-api"
877
- }, keys), {
878
- uses: telemetryOf(manifest, worker.id) !== void 0,
879
- serviceName: worker.name,
880
- ...telemetryValues ? { telemetry: telemetryValues } : {}
881
- }),
882
- NODE_ENV: "production",
883
- PORT: String(worker.port),
884
- STAGE: stage
885
- };
886
- for (const [key, value] of Object.entries(worker.env)) if (/[\r\n]/.test(value)) throw new EnvValueMultiline(worker.name, key);
887
- }
888
- const deployedTls = certificate ? {
889
- kind: "files",
890
- ...proxy === "caddy" ? {
891
- certFile: `${CADDY_TLS_DIR}/cert.pem`,
892
- keyFile: `${CADDY_TLS_DIR}/key.pem`
893
- } : edgeCertificatePaths(project)
894
- } : { kind: "acme" };
895
- const tls = local ? { kind: "internal" } : deployedTls;
896
- const layout = input.layout ?? defaultLayout(workspace.root);
897
- const compose = stackFile({
898
- project,
899
- plan,
900
- infra,
901
- apps,
902
- workers,
903
- credentials: credential.containers,
904
- ...storage ? { storage } : {},
905
- ...logs ? { logs } : {},
906
- ...redis ? { redis } : {},
907
- https,
908
- http,
909
- composeDir: join(workspace.root, stackDir(stage)),
910
- buildRoot: layout.buildRoot,
911
- workspaceRoot: workspace.root,
912
- envFiles: !input.buildOnly,
913
- proxy,
914
- edge: new Map([...edgeServices].map((service) => [service, hostOf(service)])),
915
- tls: certificate !== void 0
916
- });
917
- const routes = [
918
- ...apps.map((app) => ({
919
- name: app.name,
920
- host: app.host,
921
- upstreams: [{
922
- service: app.name,
923
- port: app.port
924
- }],
925
- streaming: true,
926
- health: app.kind === "site" ? "/" : "/health"
927
- })),
928
- ...[...fileServers.values()].map(({ route }) => route),
929
- ...logs?.host && logs.public ? [{
930
- name: LOGS_SERVICE,
931
- host: logs.host,
932
- upstreams: [{
933
- service: LOGS_SERVICE,
934
- port: LOGS_PORT
935
- }],
936
- streaming: true,
937
- allow: logs.public.allow
938
- }] : []
939
- ];
940
- const dockerfiles = {};
941
- for (const app of apps) {
942
- if (!app.build) continue;
943
- dockerfiles[app.build.dockerfile] = appDockerfile(app.name, workspace.apps[app.name], {
944
- layout,
945
- workspaceRoot: workspace.root,
946
- apps: workspace.apps,
947
- manifest,
948
- cache: STACK_CACHE,
949
- workspace,
950
- stage
951
- });
952
- }
953
- for (const worker of workers) {
954
- if (!worker.build) continue;
955
- dockerfiles[worker.build.dockerfile] = workerDockerfile({
956
- id: worker.id,
957
- name: worker.name,
958
- app: worker.host
959
- }, workspace.apps[worker.host], {
960
- layout,
961
- workspaceRoot: workspace.root,
962
- cache: STACK_CACHE
963
- });
964
- }
965
- return {
966
- project,
967
- stage,
968
- local,
969
- apps,
970
- workers,
971
- infra,
972
- compose,
973
- proxy,
974
- routes,
975
- ...proxy === "caddy" ? { caddyfile: edgeCaddyfile(routes, { tls }) } : { traefik: traefikDynamicFile(routes, {
976
- project,
977
- tls: deployedTls
978
- }) },
979
- ...certificate ? { tls: certificate } : {},
980
- dockerfiles,
981
- plan,
982
- credential,
983
- ...storage ? { storage } : {},
984
- devServices: local ? [] : devServicesUsed(services),
985
- ...logs ? { logs } : {},
986
- ...telemetry ? { telemetry } : {},
987
- ...redis ? { redis } : {}
988
- };
989
- }
990
- /** The stack's MinIO's value for one of the S3 client's keys. */
991
- function storageValue(key, storage) {
992
- if (!storage) return void 0;
993
- if (key === "AWS_ACCESS_KEY_ID") return storage.user;
994
- if (key === "AWS_SECRET_ACCESS_KEY") return storage.password;
995
- if (key === "AWS_REGION") return MINIO_REGION;
996
- }
997
- /** Which bucket an S3 credential key belongs to — the first one declared. */
998
- function storageOwner(key, plan) {
999
- if (!STORAGE_KEYS.includes(key)) return void 0;
1000
- return plan.resources.find((r) => r.kind === "objects");
1001
- }
1002
- /** The apps a stack runs: every API and every site, at its stage address. */
1003
- function stackApps(workspace, manifest, plan, options) {
1004
- const apps = [];
1005
- const resources = new Map(plan.resources.map((r) => [r.id, r]));
1006
- for (const [id, declaration] of Object.entries(manifest)) {
1007
- if (declaration.kind !== "rest-api" && declaration.kind !== "site") continue;
1008
- const name = appKey(id);
1009
- const app = workspace.apps[name];
1010
- const resource = resources.get(id);
1011
- if (!app || !resource || !app.port) continue;
1012
- const host = hostOf(name, app, workspace, resource, options);
1013
- const tag = declaration.kind === "site" ? siteTag(options.images.tag, options.stage) : options.images.tag;
1014
- const ref = validateImageRef(imageRef(options.identity, name, options.images.registry, tag));
1015
- apps.push({
1016
- app: name,
1017
- name,
1018
- id,
1019
- kind: declaration.kind,
1020
- path: app.path,
1021
- port: app.port,
1022
- host,
1023
- url: `https://${host}${options.local && options.https !== 443 ? `:${options.https}` : ""}`,
1024
- ref,
1025
- tag,
1026
- ...options.images.mode === "build" ? { build: { dockerfile: `${stackDir(options.stage)}/Dockerfile.${name}` } } : {}
1027
- });
1028
- }
1029
- return apps.sort((a, b) => a.name.localeCompare(b.name));
1030
- }
1031
- /** Each worker's image, and where it is built from. */
1032
- function stackWorkers(units, options) {
1033
- return units.map((unit) => ({
1034
- app: unit.name,
1035
- name: unit.name,
1036
- id: unit.id,
1037
- host: unit.app,
1038
- port: WORKER_PORT,
1039
- ref: validateImageRef(imageRef(options.identity, unit.name, options.images.registry, options.images.tag)),
1040
- tag: options.images.tag,
1041
- ...options.images.mode === "build" ? { build: { dockerfile: `${stackDir(options.stage)}/Dockerfile.${unit.name}` } } : {}
1042
- }));
1043
- }
1044
- /**
1045
- * The host an app answers on for a stage: the `gkm dev` edge's
1046
- * `*.localhost` names for the local stage, and the stage's domain — by the
1047
- * same rule a deploy follows — for any other.
1048
- */
1049
- function hostOf(name, app, workspace, resource, options) {
1050
- if (options.local) return hostFor(resource, workspace.name);
1051
- return resolveHost(name, app, options.stage, workspace.domains, isMainFrontendApp(name, app, workspace.apps));
1052
- }
1053
- /** The compose document: the infrastructure, the apps, and the edge. */
1054
- function stackFile(options) {
1055
- const { project, plan, apps } = options;
1056
- const infra = options.infra.filter((name) => name !== LOGS_SERVICE);
1057
- const derived = composeFor({
1058
- ...plan,
1059
- containers: [...infra]
1060
- }, {
1061
- project,
1062
- ports: Object.fromEntries(portKeys(infra, plan.fakes).map((key) => [key, 0])),
1063
- credentials: options.credentials
1064
- });
1065
- const services = {};
1066
- const volumes = {
1067
- ...derived.volumes,
1068
- ...options.proxy === "caddy" ? {
1069
- "caddy-data": {},
1070
- "caddy-config": {}
1071
- } : {},
1072
- ...options.logs ? { "openobserve-data": {} } : {}
1073
- };
1074
- for (const name of infra) {
1075
- const { ports: _ports, depends_on, profiles: _profiles, ...service } = derived.services[name];
1076
- services[name] = {
1077
- ...service,
1078
- ...depends_on?.length ? { depends_on: Object.fromEntries(depends_on.map((dep) => [dep, HEALTHY])) } : {}
1079
- };
1080
- }
1081
- const postgres = services.postgres;
1082
- if (postgres) postgres.ports = ["127.0.0.1::5432"];
1083
- const minio = services.minio;
1084
- if (minio && options.storage) {
1085
- minio.environment = {
1086
- ...minio.environment,
1087
- MINIO_ROOT_USER: options.storage.user,
1088
- MINIO_ROOT_PASSWORD: options.storage.password
1089
- };
1090
- minio.ports = ["127.0.0.1::9000"];
1091
- }
1092
- if (services["redis"] && options.redis) services[REDIS_SERVICE] = redisService(options.redis);
1093
- if (options.logs) services[LOGS_SERVICE] = logsService(options.logs);
1094
- for (const app of apps) services[app.name] = {
1095
- image: app.ref,
1096
- ...app.build ? { build: {
1097
- ...composeBuildPaths({
1098
- composeDir: options.composeDir,
1099
- buildRoot: options.buildRoot,
1100
- workspaceRoot: options.workspaceRoot,
1101
- dockerfile: app.build.dockerfile
1102
- }),
1103
- ...app.build.args ? { args: app.build.args } : {}
1104
- } } : {},
1105
- restart: "unless-stopped",
1106
- ...app.kind === "rest-api" ? {
1107
- env_file: [{
1108
- path: `./${app.name}.env`,
1109
- format: "raw"
1110
- }],
1111
- ...infra.length ? { depends_on: Object.fromEntries(infra.map((name) => [name, HEALTHY])) } : {}
1112
- } : {},
1113
- healthcheck: probe(app.port, app.kind === "site" ? "/" : "/health")
1114
- };
1115
- for (const worker of options.workers) services[worker.name] = {
1116
- image: worker.ref,
1117
- ...worker.build ? { build: composeBuildPaths({
1118
- composeDir: options.composeDir,
1119
- buildRoot: options.buildRoot,
1120
- workspaceRoot: options.workspaceRoot,
1121
- dockerfile: worker.build.dockerfile
1122
- }) } : {},
1123
- restart: "unless-stopped",
1124
- env_file: [{
1125
- path: `./${worker.name}.env`,
1126
- format: "raw"
1127
- }],
1128
- ...infra.length ? { depends_on: Object.fromEntries(infra.map((name) => [name, HEALTHY])) } : {},
1129
- healthcheck: probe(worker.port, "/health")
1130
- };
1131
- if (options.proxy === "caddy") services.caddy = {
1132
- image: DEFAULT_IMAGES.caddy,
1133
- restart: "unless-stopped",
1134
- ports: [`${options.https}:443`, `${options.http}:80`],
1135
- volumes: [
1136
- "./Caddyfile:/etc/caddy/Caddyfile:ro",
1137
- "caddy-data:/data",
1138
- "caddy-config:/config",
1139
- ...options.tls ? [`./tls:${CADDY_TLS_DIR}:ro`] : []
1140
- ],
1141
- depends_on: Object.fromEntries(apps.map((app) => [app.name, HEALTHY])),
1142
- healthcheck: {
1143
- test: [
1144
- "CMD",
1145
- "caddy",
1146
- "version"
1147
- ],
1148
- interval: "10s",
1149
- timeout: "5s",
1150
- retries: 5
1151
- }
1152
- };
1153
- for (const [service, alias] of options.edge) {
1154
- const definition = services[service];
1155
- if (!definition) continue;
1156
- definition.networks = {
1157
- default: { aliases: [alias] },
1158
- edge: { aliases: [alias] }
1159
- };
1160
- }
1161
- for (const service of Object.values(services)) {
1162
- if (!options.envFiles) delete service.env_file;
1163
- service.logging = {
1164
- driver: LOG_ROTATION.driver,
1165
- options: { ...LOG_ROTATION.options }
1166
- };
1167
- }
1168
- return {
1169
- name: project,
1170
- services,
1171
- volumes,
1172
- ...options.edge.size > 0 ? { networks: { edge: {
1173
- name: EDGE_NETWORK,
1174
- external: true
1175
- } } } : {}
1176
- };
1177
- }
1178
- /**
1179
- * A build without a layout: the workspace is the build root, with pnpm.
1180
- * What a caller that never looked at the filesystem gets.
1181
- */
1182
- function defaultLayout(root) {
1183
- return {
1184
- buildRoot: root,
1185
- gkmRoot: ".",
1186
- tools: {
1187
- packageManager: "pnpm",
1188
- turboVersion: TURBO_VERSION,
1189
- monorepo: true
1190
- },
1191
- gkmPaths: ["gkm.config.*"]
1192
- };
1193
- }
1194
- /** Render an app's env file: one `KEY=value` per line, read raw by compose. */
1195
- function envFile(env) {
1196
- return `${Object.entries(env).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value}`).join("\n")}\n`;
1197
- }
1198
- //#endregion
1199
- export { edgeComposeFile as _, composeStack as a, stack_exports as c, parseDigests as d, pinImages as f, EDGE_SERVICE as g, EDGE_PROJECT as h, composeProject as i, RegistryRequired as l, EDGE_NETWORK as m, LOGS_PORT_ENV as n, envFile as o, pinnedRef as p, LOG_ROTATION as r, stackDir as s, EDGE_PORT_ENV as t, assertImagesExist as u, traefikStaticFile as v };
1200
-
1201
- //# sourceMappingURL=stack-A6MAFZ6u.mjs.map