@intentius/chant-lexicon-terraform 0.124.54 → 0.124.56

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 (351) hide show
  1. package/README.md +7 -4
  2. package/dist/ci-setup.d.ts +11 -0
  3. package/dist/ci-setup.d.ts.map +1 -0
  4. package/dist/ci-setup.js +41 -0
  5. package/dist/components/capability-plugin.d.ts +40 -6
  6. package/dist/components/capability-plugin.d.ts.map +1 -1
  7. package/dist/components/capability-plugin.js +174 -30
  8. package/dist/components/compose-group.d.ts +1 -1
  9. package/dist/components/compose-group.d.ts.map +1 -1
  10. package/dist/components/compose-group.js +1 -1
  11. package/dist/components/release-inputs.d.ts +29 -0
  12. package/dist/components/release-inputs.d.ts.map +1 -0
  13. package/dist/components/release-inputs.js +57 -0
  14. package/dist/components/trace-facts.d.ts +16 -0
  15. package/dist/components/trace-facts.d.ts.map +1 -0
  16. package/dist/components/trace-facts.js +19 -0
  17. package/dist/cone.d.ts +53 -0
  18. package/dist/cone.d.ts.map +1 -0
  19. package/dist/cone.js +119 -0
  20. package/dist/cost-hold.d.ts +27 -0
  21. package/dist/cost-hold.d.ts.map +1 -0
  22. package/dist/cost-hold.js +21 -0
  23. package/dist/drift-rows.d.ts +61 -0
  24. package/dist/drift-rows.d.ts.map +1 -0
  25. package/dist/drift-rows.js +108 -0
  26. package/dist/exit-codes.d.ts +55 -0
  27. package/dist/exit-codes.d.ts.map +1 -0
  28. package/dist/exit-codes.js +48 -0
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -0
  32. package/dist/integrity.json +7 -4
  33. package/dist/lint/audit-catalog.d.ts.map +1 -1
  34. package/dist/lint/audit-catalog.js +27 -3
  35. package/dist/lint/audit-lineage.d.ts +14 -2
  36. package/dist/lint/audit-lineage.d.ts.map +1 -1
  37. package/dist/lint/post-synth/index.d.ts.map +1 -1
  38. package/dist/lint/post-synth/index.js +4 -0
  39. package/dist/lint/post-synth/record-store.d.ts +54 -0
  40. package/dist/lint/post-synth/record-store.d.ts.map +1 -0
  41. package/dist/lint/post-synth/record-store.js +104 -0
  42. package/dist/lint/post-synth/tf001.d.ts +2 -2
  43. package/dist/lint/post-synth/tf001.js +2 -2
  44. package/dist/lint/post-synth/tf024.d.ts +35 -11
  45. package/dist/lint/post-synth/tf024.d.ts.map +1 -1
  46. package/dist/lint/post-synth/tf024.js +92 -20
  47. package/dist/lint/post-synth/tf046.d.ts +64 -0
  48. package/dist/lint/post-synth/tf046.d.ts.map +1 -0
  49. package/dist/lint/post-synth/tf046.js +175 -0
  50. package/dist/lint/post-synth/tf047.d.ts +36 -0
  51. package/dist/lint/post-synth/tf047.d.ts.map +1 -0
  52. package/dist/lint/post-synth/tf047.js +94 -0
  53. package/dist/lock-rows.d.ts +36 -0
  54. package/dist/lock-rows.d.ts.map +1 -0
  55. package/dist/lock-rows.js +60 -0
  56. package/dist/lock-timeout.d.ts +22 -0
  57. package/dist/lock-timeout.d.ts.map +1 -0
  58. package/dist/lock-timeout.js +32 -0
  59. package/dist/manifest.json +1 -1
  60. package/dist/module-catalog/index.d.ts +69 -0
  61. package/dist/module-catalog/index.d.ts.map +1 -0
  62. package/dist/module-catalog/index.js +110 -0
  63. package/dist/module-catalog/refresh-cli.d.ts +4 -0
  64. package/dist/module-catalog/refresh-cli.d.ts.map +1 -0
  65. package/dist/module-catalog/refresh-cli.js +64 -0
  66. package/dist/module-catalog/static-tier.d.ts +6 -0
  67. package/dist/module-catalog/static-tier.d.ts.map +1 -0
  68. package/dist/module-catalog/static-tier.js +1650 -0
  69. package/dist/module-catalog/types.d.ts +65 -0
  70. package/dist/module-catalog/types.d.ts.map +1 -0
  71. package/dist/module-catalog/types.js +33 -0
  72. package/dist/okf/index.md +3 -1
  73. package/dist/okf/rules/TF024.md +2 -2
  74. package/dist/okf/rules/TF046.md +11 -0
  75. package/dist/okf/rules/TF047.md +11 -0
  76. package/dist/op/activities/terraform.d.ts +29 -0
  77. package/dist/op/activities/terraform.d.ts.map +1 -1
  78. package/dist/op/activities/terraform.js +19 -4
  79. package/dist/pin/consumers.d.ts +35 -0
  80. package/dist/pin/consumers.d.ts.map +1 -0
  81. package/dist/pin/consumers.js +70 -0
  82. package/dist/plugin.d.ts.map +1 -1
  83. package/dist/plugin.js +5 -0
  84. package/dist/registry/address.d.ts +63 -0
  85. package/dist/registry/address.d.ts.map +1 -0
  86. package/dist/registry/address.js +65 -0
  87. package/dist/registry/catalog-read.d.ts +14 -0
  88. package/dist/registry/catalog-read.d.ts.map +1 -0
  89. package/dist/registry/catalog-read.js +58 -0
  90. package/dist/registry/deprecated.d.ts +37 -0
  91. package/dist/registry/deprecated.d.ts.map +1 -0
  92. package/dist/registry/deprecated.js +43 -0
  93. package/dist/registry/docs.d.ts +96 -0
  94. package/dist/registry/docs.d.ts.map +1 -0
  95. package/dist/registry/docs.js +210 -0
  96. package/dist/registry/edge/edge.d.ts +46 -0
  97. package/dist/registry/edge/edge.d.ts.map +1 -0
  98. package/dist/registry/edge/edge.js +194 -0
  99. package/dist/registry/edge/index.d.ts +11 -0
  100. package/dist/registry/edge/index.d.ts.map +1 -0
  101. package/dist/registry/edge/index.js +10 -0
  102. package/dist/registry/edge/lambda-edge.d.ts +76 -0
  103. package/dist/registry/edge/lambda-edge.d.ts.map +1 -0
  104. package/dist/registry/edge/lambda-edge.js +95 -0
  105. package/dist/registry/edge/sigv4-web.d.ts +33 -0
  106. package/dist/registry/edge/sigv4-web.d.ts.map +1 -0
  107. package/dist/registry/edge/sigv4-web.js +56 -0
  108. package/dist/registry/edge/token-source.d.ts +73 -0
  109. package/dist/registry/edge/token-source.d.ts.map +1 -0
  110. package/dist/registry/edge/token-source.js +115 -0
  111. package/dist/registry/edge/worker.d.ts +45 -0
  112. package/dist/registry/edge/worker.d.ts.map +1 -0
  113. package/dist/registry/edge/worker.js +26 -0
  114. package/dist/registry/handler.d.ts +25 -0
  115. package/dist/registry/handler.d.ts.map +1 -0
  116. package/dist/registry/handler.js +75 -0
  117. package/dist/registry/headers-file.d.ts +21 -0
  118. package/dist/registry/headers-file.d.ts.map +1 -0
  119. package/dist/registry/headers-file.js +57 -0
  120. package/dist/registry/index.d.ts +31 -0
  121. package/dist/registry/index.d.ts.map +1 -0
  122. package/dist/registry/index.js +30 -0
  123. package/dist/registry/mirror-paths.d.ts +9 -0
  124. package/dist/registry/mirror-paths.d.ts.map +1 -0
  125. package/dist/registry/mirror-paths.js +8 -0
  126. package/dist/registry/mirror.d.ts +130 -0
  127. package/dist/registry/mirror.d.ts.map +1 -0
  128. package/dist/registry/mirror.js +319 -0
  129. package/dist/registry/proxies.d.ts +73 -0
  130. package/dist/registry/proxies.d.ts.map +1 -0
  131. package/dist/registry/proxies.js +111 -0
  132. package/dist/registry/reader.d.ts +57 -0
  133. package/dist/registry/reader.d.ts.map +1 -0
  134. package/dist/registry/reader.js +198 -0
  135. package/dist/registry/semver.d.ts +16 -0
  136. package/dist/registry/semver.d.ts.map +1 -0
  137. package/dist/registry/semver.js +24 -0
  138. package/dist/registry/serve.d.ts +24 -0
  139. package/dist/registry/serve.d.ts.map +1 -0
  140. package/dist/registry/serve.js +64 -0
  141. package/dist/registry/source-forms.d.ts +79 -0
  142. package/dist/registry/source-forms.d.ts.map +1 -0
  143. package/dist/registry/source-forms.js +202 -0
  144. package/dist/registry/targets/artifactory.d.ts +28 -0
  145. package/dist/registry/targets/artifactory.d.ts.map +1 -0
  146. package/dist/registry/targets/artifactory.js +206 -0
  147. package/dist/registry/targets/fake-artifactory.d.ts +30 -0
  148. package/dist/registry/targets/fake-artifactory.d.ts.map +1 -0
  149. package/dist/registry/targets/fake-artifactory.js +142 -0
  150. package/dist/registry/targets/fake-gitlab.d.ts +39 -0
  151. package/dist/registry/targets/fake-gitlab.d.ts.map +1 -0
  152. package/dist/registry/targets/fake-gitlab.js +136 -0
  153. package/dist/registry/targets/fake-hcp.d.ts +50 -0
  154. package/dist/registry/targets/fake-hcp.d.ts.map +1 -0
  155. package/dist/registry/targets/fake-hcp.js +202 -0
  156. package/dist/registry/targets/fake-http.d.ts +31 -0
  157. package/dist/registry/targets/fake-http.d.ts.map +1 -0
  158. package/dist/registry/targets/fake-http.js +87 -0
  159. package/dist/registry/targets/fake-nexus.d.ts +27 -0
  160. package/dist/registry/targets/fake-nexus.d.ts.map +1 -0
  161. package/dist/registry/targets/fake-nexus.js +137 -0
  162. package/dist/registry/targets/gitlab.d.ts +53 -0
  163. package/dist/registry/targets/gitlab.d.ts.map +1 -0
  164. package/dist/registry/targets/gitlab.js +187 -0
  165. package/dist/registry/targets/hcp.d.ts +29 -0
  166. package/dist/registry/targets/hcp.d.ts.map +1 -0
  167. package/dist/registry/targets/hcp.js +310 -0
  168. package/dist/registry/targets/hosted.d.ts +62 -0
  169. package/dist/registry/targets/hosted.d.ts.map +1 -0
  170. package/dist/registry/targets/hosted.js +74 -0
  171. package/dist/registry/targets/index.d.ts +18 -0
  172. package/dist/registry/targets/index.d.ts.map +1 -0
  173. package/dist/registry/targets/index.js +17 -0
  174. package/dist/registry/targets/module-target.d.ts +68 -0
  175. package/dist/registry/targets/module-target.d.ts.map +1 -0
  176. package/dist/registry/targets/module-target.js +63 -0
  177. package/dist/registry/targets/nexus.d.ts +43 -0
  178. package/dist/registry/targets/nexus.d.ts.map +1 -0
  179. package/dist/registry/targets/nexus.js +250 -0
  180. package/dist/registry/targets/zip.d.ts +55 -0
  181. package/dist/registry/targets/zip.d.ts.map +1 -0
  182. package/dist/registry/targets/zip.js +278 -0
  183. package/dist/registry/writer.d.ts +101 -0
  184. package/dist/registry/writer.d.ts.map +1 -0
  185. package/dist/registry/writer.js +109 -0
  186. package/dist/rules/record-store.ts +122 -0
  187. package/dist/rules/tf001.ts +2 -2
  188. package/dist/rules/tf024.ts +105 -22
  189. package/dist/rules/tf046.ts +204 -0
  190. package/dist/rules/tf047.ts +114 -0
  191. package/dist/state/azure-blob.d.ts +8 -67
  192. package/dist/state/azure-blob.d.ts.map +1 -1
  193. package/dist/state/azure-blob.js +6 -199
  194. package/dist/state/gcs.d.ts +7 -61
  195. package/dist/state/gcs.d.ts.map +1 -1
  196. package/dist/state/gcs.js +5 -222
  197. package/dist/state/object-store.d.ts +4 -33
  198. package/dist/state/object-store.d.ts.map +1 -1
  199. package/dist/state/object-store.js +4 -17
  200. package/dist/state/s3.d.ts +11 -58
  201. package/dist/state/s3.d.ts.map +1 -1
  202. package/dist/state/s3.js +10 -161
  203. package/dist/state/store.js +2 -2
  204. package/dist/state-ops.d.ts +255 -0
  205. package/dist/state-ops.d.ts.map +1 -0
  206. package/dist/state-ops.js +305 -0
  207. package/dist/state-reads.d.ts +57 -0
  208. package/dist/state-reads.d.ts.map +1 -0
  209. package/dist/state-reads.js +104 -0
  210. package/dist/used-sources.d.ts +54 -0
  211. package/dist/used-sources.d.ts.map +1 -0
  212. package/dist/used-sources.js +157 -0
  213. package/package.json +19 -3
  214. package/src/__fixtures__/drift-record/README.md +5 -0
  215. package/src/__fixtures__/drift-rows/live-root-plan.json +70 -0
  216. package/src/__fixtures__/drift-rows/refresh-plan.json +61 -0
  217. package/src/__fixtures__/used-sources/root/extra.tofu +1 -0
  218. package/src/__fixtures__/used-sources/root/main.tf +31 -0
  219. package/src/__fixtures__/used-sources/root/modules/net/main.tf +14 -0
  220. package/src/__fixtures__/used-sources/root/modules/net/versions.tf.json +7 -0
  221. package/src/__fixtures__/used-sources/unit/terragrunt.hcl +3 -0
  222. package/src/ci-setup.ts +42 -0
  223. package/src/components/capability-plugin.test.ts +26 -14
  224. package/src/components/capability-plugin.ts +185 -31
  225. package/src/components/compose-group.ts +1 -1
  226. package/src/components/pr-loop-compose.tofu.e2e.test.ts +1 -1
  227. package/src/components/pr-loop.tofu.test.ts +1 -1
  228. package/src/components/release-inputs.ts +69 -0
  229. package/src/components/trace-facts.test.ts +21 -0
  230. package/src/components/trace-facts.ts +27 -0
  231. package/src/cone.ts +138 -0
  232. package/src/cost-hold.ts +29 -0
  233. package/src/describe-resources.engine-facts.test.ts +2 -3
  234. package/src/drift-rows.test.ts +135 -0
  235. package/src/drift-rows.ts +137 -0
  236. package/src/exit-codes.test.ts +23 -0
  237. package/src/exit-codes.ts +59 -0
  238. package/src/index.ts +3 -0
  239. package/src/lint/audit-catalog.ts +29 -3
  240. package/src/lint/audit-lineage.ts +14 -2
  241. package/src/lint/post-synth/fixtures/TF024/live-with-record-store/main.tf +18 -0
  242. package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/estate.chdf.hcl +1 -0
  243. package/src/lint/post-synth/fixtures/TF024/sidecar-with-backend/main.tf +10 -0
  244. package/src/lint/post-synth/fixtures/TF046/azurerm-no-resource-group/main.tf +15 -0
  245. package/src/lint/post-synth/fixtures/TF046/backend-config-only/main.tf +14 -0
  246. package/src/lint/post-synth/fixtures/TF046/derived-s3/backend.tf +7 -0
  247. package/src/lint/post-synth/fixtures/TF046/derived-s3/main.tf +9 -0
  248. package/src/lint/post-synth/fixtures/TF046/explicit-local/main.tf +16 -0
  249. package/src/lint/post-synth/fixtures/TF046/explicit-s3-beside-local-backend/main.tf +15 -0
  250. package/src/lint/post-synth/fixtures/TF046/implied-local/main.tf +9 -0
  251. package/src/lint/post-synth/fixtures/TF046/sidecar-implied/estate.chdf.hcl +1 -0
  252. package/src/lint/post-synth/fixtures/TF046/sidecar-implied/main.tf +5 -0
  253. package/src/lint/post-synth/fixtures/TF047/clear/main.tf +12 -0
  254. package/src/lint/post-synth/fixtures/TF047/refused/main.tf +12 -0
  255. package/src/lint/post-synth/fixtures/TF047/unknown-module/main.tf +12 -0
  256. package/src/lint/post-synth/fixtures/TF047/unknown-version/main.tf +12 -0
  257. package/src/lint/post-synth/index.ts +4 -0
  258. package/src/lint/post-synth/record-store.ts +122 -0
  259. package/src/lint/post-synth/tf001.ts +2 -2
  260. package/src/lint/post-synth/tf024.test.ts +26 -5
  261. package/src/lint/post-synth/tf024.ts +105 -22
  262. package/src/lint/post-synth/tf046.test.ts +137 -0
  263. package/src/lint/post-synth/tf046.ts +204 -0
  264. package/src/lint/post-synth/tf047.test.ts +56 -0
  265. package/src/lint/post-synth/tf047.ts +114 -0
  266. package/src/lock-rows.test.ts +70 -0
  267. package/src/lock-rows.ts +82 -0
  268. package/src/lock-timeout.ts +34 -0
  269. package/src/module-catalog/index.test.ts +93 -0
  270. package/src/module-catalog/index.ts +159 -0
  271. package/src/module-catalog/refresh-cli.ts +68 -0
  272. package/src/module-catalog/static-tier.ts +1659 -0
  273. package/src/module-catalog/types.ts +88 -0
  274. package/src/op/activities/terraform.ts +46 -5
  275. package/src/op-wave-step.test.ts +109 -0
  276. package/src/pin/__fixtures__/consumers/live/dev/main.tf +4 -0
  277. package/src/pin/__fixtures__/consumers/live/prod/main.tf +21 -0
  278. package/src/pin/__fixtures__/consumers/units/vpc/terragrunt.hcl +7 -0
  279. package/src/pin/consumers.test.ts +57 -0
  280. package/src/pin/consumers.ts +95 -0
  281. package/src/plugin.ts +6 -0
  282. package/src/registry/__fixtures__/docs/documented/.terraform-docs.yml +3 -0
  283. package/src/registry/__fixtures__/docs/documented/README.md +1 -0
  284. package/src/registry/__fixtures__/docs/documented/main.tf +8 -0
  285. package/src/registry/__fixtures__/docs/json/main.tf.json +14 -0
  286. package/src/registry/__fixtures__/docs/vpc/README.md +3 -0
  287. package/src/registry/__fixtures__/docs/vpc/examples/complete/main.tf +5 -0
  288. package/src/registry/__fixtures__/docs/vpc/examples/simple/README.md +1 -0
  289. package/src/registry/__fixtures__/docs/vpc/examples/simple/main.tf +4 -0
  290. package/src/registry/__fixtures__/docs/vpc/main.tf +14 -0
  291. package/src/registry/__fixtures__/docs/vpc/outputs.tf +9 -0
  292. package/src/registry/__fixtures__/docs/vpc/variables.tf +16 -0
  293. package/src/registry/__fixtures__/proxied-root/main.tf +12 -0
  294. package/src/registry/address.ts +100 -0
  295. package/src/registry/catalog-read.ts +56 -0
  296. package/src/registry/deprecated.test.ts +139 -0
  297. package/src/registry/deprecated.ts +76 -0
  298. package/src/registry/docs.test.ts +140 -0
  299. package/src/registry/docs.ts +299 -0
  300. package/src/registry/edge/bundle.test.ts +50 -0
  301. package/src/registry/edge/edge.test.ts +176 -0
  302. package/src/registry/edge/edge.ts +211 -0
  303. package/src/registry/edge/index.ts +10 -0
  304. package/src/registry/edge/lambda-edge.ts +127 -0
  305. package/src/registry/edge/sigv4-web.ts +79 -0
  306. package/src/registry/edge/token-source.test.ts +258 -0
  307. package/src/registry/edge/token-source.ts +169 -0
  308. package/src/registry/edge/worker.ts +61 -0
  309. package/src/registry/handler.ts +89 -0
  310. package/src/registry/headers-file.ts +61 -0
  311. package/src/registry/index.ts +31 -0
  312. package/src/registry/mirror-paths.ts +10 -0
  313. package/src/registry/mirror.test.ts +198 -0
  314. package/src/registry/mirror.ts +395 -0
  315. package/src/registry/proxies.test.ts +153 -0
  316. package/src/registry/proxies.ts +132 -0
  317. package/src/registry/reader.ts +225 -0
  318. package/src/registry/registry.test.ts +262 -0
  319. package/src/registry/semver.ts +34 -0
  320. package/src/registry/serve.ts +82 -0
  321. package/src/registry/source-forms.test.ts +87 -0
  322. package/src/registry/source-forms.ts +216 -0
  323. package/src/registry/targets/artifactory.test.ts +248 -0
  324. package/src/registry/targets/artifactory.ts +257 -0
  325. package/src/registry/targets/fake-artifactory.ts +158 -0
  326. package/src/registry/targets/fake-gitlab.ts +161 -0
  327. package/src/registry/targets/fake-hcp.ts +233 -0
  328. package/src/registry/targets/fake-http.ts +97 -0
  329. package/src/registry/targets/fake-nexus.ts +152 -0
  330. package/src/registry/targets/gitlab.test.ts +208 -0
  331. package/src/registry/targets/gitlab.ts +223 -0
  332. package/src/registry/targets/hcp.test.ts +230 -0
  333. package/src/registry/targets/hcp.ts +360 -0
  334. package/src/registry/targets/hosted.ts +117 -0
  335. package/src/registry/targets/index.ts +17 -0
  336. package/src/registry/targets/module-target.ts +131 -0
  337. package/src/registry/targets/nexus.test.ts +220 -0
  338. package/src/registry/targets/nexus.ts +314 -0
  339. package/src/registry/targets/zip.test.ts +65 -0
  340. package/src/registry/targets/zip.ts +287 -0
  341. package/src/registry/writer.ts +155 -0
  342. package/src/state/azure-blob.ts +6 -221
  343. package/src/state/gcs.ts +5 -252
  344. package/src/state/object-store.ts +4 -32
  345. package/src/state/s3.ts +10 -182
  346. package/src/state/store.ts +2 -2
  347. package/src/state-ops.test.ts +182 -0
  348. package/src/state-ops.ts +462 -0
  349. package/src/state-reads.ts +116 -0
  350. package/src/used-sources.test.ts +75 -0
  351. package/src/used-sources.ts +204 -0
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The Terraform module registry protocol (chant#4565): addresses, the writer
3
+ * of the protocol's files, the reader as Terraform and OpenTofu read them,
4
+ * and a handler that serves the files with the download header both CLIs
5
+ * install from. Moved from terragucci's `publish/registry.ts`; products keep
6
+ * their config keys and presets and call this. The module targets that
7
+ * publish a version to a hosted registry (GitLab first) are under ./targets
8
+ * (chant#4603). ./docs reads a module's README, inputs, outputs and
9
+ * examples for its release catalog page (chant#4598). ./source-forms puts
10
+ * module sources and provider addresses in one normal form, and
11
+ * ../used-sources lists the ones a root uses (chant#3924); ./proxies
12
+ * reads them through Artifactory and Nexus proxies (chant#4606).
13
+ */
14
+
15
+ export * from "./address";
16
+ export * from "./semver";
17
+ export * from "./writer";
18
+ export * from "./reader";
19
+ export * from "./handler";
20
+ export * from "./serve";
21
+ export * from "./headers-file";
22
+ export * from "./mirror";
23
+ export * from "./edge";
24
+ export * from "./targets";
25
+ export * from "./docs";
26
+ export * from "./catalog-read";
27
+ export * from "./deprecated";
28
+ export * from "./source-forms";
29
+ export * from "./proxies";
30
+ export { moduleConsumers, isLocalSource, type ModuleConsumer } from "../pin/consumers";
31
+ export { usedSources, type UsedSource, type UnreadSource, type UsedSourcesOptions } from "../used-sources";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Paths of the provider network mirror under a registry (chant#4564), with no
3
+ * Node imports, so the edge function can tell a mirror's version file apart.
4
+ */
5
+
6
+ /** The mirror's place under the registry's prefix. */
7
+ export const PROVIDER_MIRROR_PREFIX = "providers";
8
+
9
+ /** True for a mirror's `providers/<host>/<ns>/<type>/<version>.json` path, with or without a prefix in front. */
10
+ export const isMirrorVersionPath = (path: string): boolean => new RegExp(`(^|/)${PROVIDER_MIRROR_PREFIX}/[^/]+/[^/]+/[^/]+/(?!index\\.json$)[^/]+\\.json$`).test(path.split("?")[0]!);
@@ -0,0 +1,198 @@
1
+ // The provider network mirror served from the module registry's bucket (chant#4564). `providers mirror`
2
+ // is replaced by a fake that writes the layout the binaries write, so nothing here downloads a provider.
3
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { describe, expect, it } from "vitest";
7
+ import { registryEdge, signLocation } from "./edge";
8
+ import { handlerFetch, registryHandler, storeFiles } from "./handler";
9
+ import {
10
+ LOCK_FILE,
11
+ buildMirror,
12
+ isMirrorVersionPath,
13
+ lockFiles,
14
+ mirrorCliConfig,
15
+ mirrorGroups,
16
+ mirrorHas,
17
+ mirrorKey,
18
+ mirrorModules,
19
+ mirrorUrl,
20
+ otherHostAddress,
21
+ uploadMirror,
22
+ type MirrorRun,
23
+ type MirrorUploadStore,
24
+ } from "./mirror";
25
+
26
+ const lock = (providers: Record<string, string>): string =>
27
+ Object.entries(providers)
28
+ .map(([a, v]) => `provider "${a}" {\n version = "${v}"\n constraints = "~> ${v}"\n hashes = [\n "h1:x=",\n ]\n}\n`)
29
+ .join("\n");
30
+
31
+ function repo(): string {
32
+ const root = mkdtempSync(join(tmpdir(), "chant-mirror-repo-"));
33
+ const put = (path: string, text: string) => {
34
+ mkdirSync(dirname(join(root, path)), { recursive: true });
35
+ writeFileSync(join(root, path), text);
36
+ };
37
+ put(`live/a/${LOCK_FILE}`, lock({ "registry.opentofu.org/hashicorp/aws": "5.0.0" }));
38
+ put(`live/b/${LOCK_FILE}`, lock({ "registry.opentofu.org/hashicorp/aws": "5.0.0" }));
39
+ put(`live/c/${LOCK_FILE}`, lock({ "registry.terraform.io/hashicorp/aws": "5.1.0", "example.com/acme/thing": "1.0.0" }));
40
+ put(`live/c/.terraform/providers/${LOCK_FILE}`, lock({ "registry.terraform.io/x/y": "9.9.9" }));
41
+ put(`live/d/${LOCK_FILE}`, "# nothing locked\n");
42
+ return root;
43
+ }
44
+
45
+ /** Writes what `providers mirror` writes for each required provider, one zip per platform. */
46
+ const fakeMirror = (calls: string[][]): MirrorRun => (_bin, args) => {
47
+ calls.push(args);
48
+ const chdir = args.find((a) => a.startsWith("-chdir="))!.slice("-chdir=".length);
49
+ const out = args[args.length - 1]!;
50
+ const platforms = args.filter((a) => a.startsWith("-platform=")).map((a) => a.slice("-platform=".length));
51
+ const text = readFileSync(join(chdir, "versions.tf"), "utf-8");
52
+ for (const m of text.matchAll(/source = "([^"]+)", version = "= ([^"]+)"/g)) {
53
+ const [address, version] = [m[1]!, m[2]!];
54
+ const type = address.split("/").pop()!;
55
+ const dir = join(out, ...address.split("/"));
56
+ mkdirSync(dir, { recursive: true });
57
+ const archives: Record<string, { url: string; hashes: string[] }> = {};
58
+ for (const p of platforms) {
59
+ const zip = `terraform-provider-${type}_${version}_${p}.zip`;
60
+ writeFileSync(join(dir, zip), `zip ${address} ${version} ${p}`);
61
+ archives[p] = { url: zip, hashes: ["h1:x="] };
62
+ }
63
+ writeFileSync(join(dir, `${version}.json`), JSON.stringify({ archives }));
64
+ // What the binary writes for this run only; buildMirror rewrites it from every version file.
65
+ writeFileSync(join(dir, "index.json"), JSON.stringify({ versions: { [version]: {} } }));
66
+ }
67
+ return { status: 0, output: "" };
68
+ };
69
+
70
+ function memoryStore(): MirrorUploadStore & { objects: Map<string, string | Buffer>; puts: string[] } {
71
+ const objects = new Map<string, string | Buffer>();
72
+ const puts: string[] = [];
73
+ return {
74
+ objects,
75
+ puts,
76
+ async put(key, body) {
77
+ puts.push(key);
78
+ objects.set(key, body);
79
+ return {};
80
+ },
81
+ async read(key) {
82
+ const b = objects.get(key);
83
+ return b === undefined ? {} : { body: b.toString() };
84
+ },
85
+ async has(key) {
86
+ return objects.has(key);
87
+ },
88
+ };
89
+ }
90
+
91
+ describe("lock files and groups (shared with the image recipe)", () => {
92
+ it("finds lock files outside caches and groups the ones that pin the same providers", () => {
93
+ const root = repo();
94
+ expect(lockFiles(root)).toEqual([`live/a/${LOCK_FILE}`, `live/b/${LOCK_FILE}`, `live/c/${LOCK_FILE}`, `live/d/${LOCK_FILE}`]);
95
+ const groups = mirrorGroups(root);
96
+ expect(groups.map((g) => g.locks)).toEqual([[`live/a/${LOCK_FILE}`, `live/b/${LOCK_FILE}`], [`live/c/${LOCK_FILE}`]]);
97
+ });
98
+
99
+ it("writes a module per group at exactly the locked versions, with the other default host when asked", () => {
100
+ const groups = mirrorGroups(repo());
101
+ const plain = mirrorModules(groups);
102
+ expect(plain.map((f) => f.path)).toEqual(["mirror/01/versions.tf", `mirror/01/${LOCK_FILE}`, "mirror/02/versions.tf", `mirror/02/${LOCK_FILE}`]);
103
+ expect(plain[0]!.content).toContain('source = "registry.opentofu.org/hashicorp/aws", version = "= 5.0.0"');
104
+ expect(plain[0]!.content).not.toContain("registry.terraform.io");
105
+ const both = mirrorModules(groups, { bothHosts: true });
106
+ expect(both[0]!.content).toContain('source = "registry.terraform.io/hashicorp/aws", version = "= 5.0.0"');
107
+ expect(both[2]!.content).toContain('source = "registry.opentofu.org/hashicorp/aws", version = "= 5.1.0"');
108
+ expect(both[2]!.content).not.toContain("registry.opentofu.org/acme/thing");
109
+ expect(otherHostAddress("example.com/acme/thing")).toBeUndefined();
110
+ });
111
+ });
112
+
113
+ describe("buildMirror", () => {
114
+ it("runs providers mirror per group for each platform and lists every version in index.json", () => {
115
+ const root = repo();
116
+ const out = mkdtempSync(join(tmpdir(), "chant-mirror-out-"));
117
+ const calls: string[][] = [];
118
+ const built = buildMirror(root, out, { binary: "tofu", platforms: ["linux/amd64", "darwin_arm64"], run: fakeMirror(calls) });
119
+ expect(calls).toHaveLength(2);
120
+ expect(calls[0]!.slice(1, 5)).toEqual(["providers", "mirror", "-platform=linux_amd64", "-platform=darwin_arm64"]);
121
+ expect(built.providers).toContain("registry.terraform.io/hashicorp/aws 5.0.0");
122
+ expect(built.providers).toContain("registry.opentofu.org/hashicorp/aws 5.1.0");
123
+ expect(mirrorHas(out, "registry.opentofu.org/hashicorp/aws", "5.0.0", "linux_amd64")).toBe(true);
124
+ // Both groups mirrored a version of registry.terraform.io/hashicorp/aws; the index lists both.
125
+ const index = JSON.parse(readFileSync(join(out, "registry.terraform.io/hashicorp/aws/index.json"), "utf-8"));
126
+ expect(Object.keys(index.versions)).toEqual(["5.0.0", "5.1.0"]);
127
+ });
128
+
129
+ it("says which group failed", () => {
130
+ const fail: MirrorRun = () => ({ status: 1, output: "Error: no such provider" });
131
+ expect(() => buildMirror(repo(), mkdtempSync(join(tmpdir(), "chant-mirror-out-")), { run: fail })).toThrow("providers mirror for mirror/01 failed (exit 1)");
132
+ expect(() => buildMirror(repo(), mkdtempSync(join(tmpdir(), "chant-mirror-out-")), { platforms: ["linux"], run: fail })).toThrow("os_arch");
133
+ });
134
+ });
135
+
136
+ describe("uploadMirror", () => {
137
+ it("puts archives first and index.json last, under the registry's prefix, merging the bucket's index", async () => {
138
+ const out = mkdtempSync(join(tmpdir(), "chant-mirror-out-"));
139
+ buildMirror(repo(), out, { run: fakeMirror([]) });
140
+ const store = memoryStore();
141
+ const indexKey = mirrorKey("registry", "registry.opentofu.org/hashicorp/aws/index.json");
142
+ store.objects.set(indexKey, JSON.stringify({ versions: { "4.9.0": {} } }));
143
+ const first = await uploadMirror(store, out, { prefix: "registry" });
144
+ const lastArchive = Math.max(...first.put.map((k, i) => (k.endsWith(".zip") ? i : -1)));
145
+ const firstIndex = first.put.findIndex((k) => k.endsWith("/index.json"));
146
+ expect(lastArchive).toBeLessThan(firstIndex);
147
+ expect(first.put.every((k) => k.startsWith("registry/providers/"))).toBe(true);
148
+ // The mirror covers both default hosts (#4564), so live/c's registry.terraform.io 5.1.0 is mirrored here too.
149
+ expect(Object.keys(JSON.parse(store.objects.get(indexKey)!.toString()).versions)).toEqual(["4.9.0", "5.0.0", "5.1.0"]);
150
+ const again = await uploadMirror(store, out, { prefix: "registry" });
151
+ expect(again.kept.length).toBeGreaterThan(0);
152
+ expect(again.put.some((k) => k.endsWith(".zip"))).toBe(false);
153
+ });
154
+ });
155
+
156
+ describe("serving the mirror", () => {
157
+ it("serves index.json, the version file and the archive through the registry handler", async () => {
158
+ const out = mkdtempSync(join(tmpdir(), "chant-mirror-out-"));
159
+ buildMirror(repo(), out, { platforms: ["linux_amd64"], run: fakeMirror([]) });
160
+ const store = memoryStore();
161
+ await uploadMirror(store, out);
162
+ const fetch = handlerFetch(registryHandler(storeFiles({ read: async (k) => ({ body: store.objects.get(k) as string | Uint8Array | undefined }) })));
163
+ const base = mirrorUrl("https://modules.test");
164
+ expect(base).toBe("https://modules.test/providers/");
165
+ const index = await (await fetch(`${base}registry.opentofu.org/hashicorp/aws/index.json`)).json();
166
+ expect(index).toEqual({ versions: { "5.0.0": {}, "5.1.0": {} } });
167
+ const version = (await (await fetch(`${base}registry.opentofu.org/hashicorp/aws/5.0.0.json`)).json()) as { archives: Record<string, { url: string }> };
168
+ const zip = await fetch(new URL(version.archives.linux_amd64!.url, `${base}registry.opentofu.org/hashicorp/aws/5.0.0.json`).toString());
169
+ expect(zip.status).toBe(200);
170
+ });
171
+
172
+ it("with the edge function's auth on, signs each archive URL in a version file", async () => {
173
+ const out = mkdtempSync(join(tmpdir(), "chant-mirror-out-"));
174
+ buildMirror(repo(), out, { platforms: ["linux_amd64"], run: fakeMirror([]) });
175
+ const store = memoryStore();
176
+ await uploadMirror(store, out);
177
+ const now = 1_800_000_000_000;
178
+ const fetch = handlerFetch(registryEdge({ files: async (k) => ({ body: store.objects.get(k) as string | Uint8Array | undefined }), auth: { tokens: ["s3cret"] }, now: () => now }));
179
+ const versionUrl = "https://modules.test/providers/registry.opentofu.org/hashicorp/aws/5.0.0.json";
180
+ expect(isMirrorVersionPath(new URL(versionUrl).pathname.slice(1))).toBe(true);
181
+ expect((await fetch(versionUrl)).status).toBe(401);
182
+ const doc = (await (await fetch(versionUrl, { headers: { authorization: "Bearer s3cret" } })).json()) as { archives: Record<string, { url: string }> };
183
+ const signed = doc.archives.linux_amd64!.url;
184
+ expect(signed).toContain("signature=");
185
+ expect((await fetch(signed)).status).toBe(200);
186
+ expect((await fetch(signed.split("?")[0]!)).status).toBe(401);
187
+ expect(await signLocation(signed.split("?")[0]!, "s3cret", now)).toBe(signed);
188
+ });
189
+ });
190
+
191
+ describe("mirrorCliConfig", () => {
192
+ it("sends the mirrored providers to the mirror only and the rest direct", () => {
193
+ const text = mirrorCliConfig("https://modules.test/providers", ["registry.terraform.io/hashicorp/aws 5.0.0", "registry.opentofu.org/hashicorp/aws"]);
194
+ expect(text).toContain('url = "https://modules.test/providers/"');
195
+ expect(text).not.toContain("5.0.0");
196
+ expect(text).toMatch(/network_mirror \{[\s\S]*include = \[[\s\S]*"registry.opentofu.org\/hashicorp\/aws",[\s\S]*direct \{[\s\S]*exclude = \[/);
197
+ });
198
+ });
@@ -0,0 +1,395 @@
1
+ /**
2
+ * A provider network mirror served from the module registry's bucket
3
+ * (chant#4564), and the lock-file grouping terragucci's image recipe uses for
4
+ * its `filesystem_mirror` (moved from terragucci `image-recipe.ts:225-232`,
5
+ * 18f6431595), so both build from one implementation.
6
+ *
7
+ * The network mirror protocol is the layout `providers mirror` writes, served
8
+ * over HTTPS (https://developer.hashicorp.com/terraform/internals/provider-network-mirror-protocol):
9
+ *
10
+ * <host>/<namespace>/<type>/index.json {"versions": {"<v>": {}}}
11
+ * <host>/<namespace>/<type>/<v>.json {"archives": {"<os>_<arch>": {"url": ..., "hashes": ["h1:...", "zh:..."]}}}
12
+ * <host>/<namespace>/<type>/terraform-provider-<type>_<v>_<os>_<arch>.zip
13
+ *
14
+ * `buildMirror` writes one module per distinct lock file, each holding its
15
+ * providers at exactly the locked version beside that lock file, and runs
16
+ * `providers mirror` on each for the platforms asked for, so the lock file's
17
+ * hashes check every download. `uploadMirror` puts the result under the
18
+ * registry's prefix at `providers/`, archives first and `index.json` last, and
19
+ * merges each `index.json` with the one already in the bucket.
20
+ *
21
+ * Lock files keep the host the provider came from. A hostless source resolves
22
+ * to registry.terraform.io under Terraform and to registry.opentofu.org under
23
+ * OpenTofu and choudoufu, so by default each provider locked on one of those
24
+ * two hosts is mirrored under the other as well.
25
+ */
26
+ import { spawnSync } from "node:child_process";
27
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
28
+ import { tmpdir } from "node:os";
29
+ import { join, posix, relative } from "node:path";
30
+ import { keyFor } from "./address";
31
+ import { PROVIDER_MIRROR_PREFIX } from "./mirror-paths";
32
+ import type { RegistryWriteStore } from "./writer";
33
+
34
+ export { PROVIDER_MIRROR_PREFIX, isMirrorVersionPath } from "./mirror-paths";
35
+
36
+ export const LOCK_FILE = ".terraform.lock.hcl";
37
+ /** The two hosts a hostless provider source resolves to. */
38
+ export const TERRAFORM_REGISTRY = "registry.terraform.io";
39
+ export const OPENTOFU_REGISTRY = "registry.opentofu.org";
40
+
41
+ /** Directories a lock file is never looked for in: caches and what init or a package manager writes. */
42
+ const SKIP = new Set([".git", ".terraform", ".terragrunt-cache", "node_modules", "cdktf.out", ".terragucci"]);
43
+
44
+ /** Every provider in a lock file, by its full address, with its version. */
45
+ export function readLock(text: string): Map<string, string> {
46
+ const out = new Map<string, string>();
47
+ for (const m of text.matchAll(/provider\s+"([^"]+)"\s*\{([^}]*)\}/g)) {
48
+ const v = /\bversion\s*=\s*"([^"]+)"/.exec(m[2]!);
49
+ if (v) out.set(m[1]!, v[1]!);
50
+ }
51
+ return out;
52
+ }
53
+
54
+ /** The repo's lock files, as `/`-separated paths relative to the repo, sorted. */
55
+ export function lockFiles(repo: string): string[] {
56
+ const out: string[] = [];
57
+ const walk = (dir: string): void => {
58
+ let names: string[];
59
+ try {
60
+ names = readdirSync(dir);
61
+ } catch {
62
+ return;
63
+ }
64
+ for (const n of names) {
65
+ const p = join(dir, n);
66
+ if (n === LOCK_FILE) {
67
+ out.push(posix.normalize(relative(repo, p).split("\\").join("/")));
68
+ continue;
69
+ }
70
+ if (SKIP.has(n)) continue;
71
+ let dirLike = false;
72
+ try {
73
+ dirLike = statSync(p).isDirectory();
74
+ } catch {
75
+ dirLike = false;
76
+ }
77
+ if (dirLike) walk(p);
78
+ }
79
+ };
80
+ walk(repo);
81
+ return out.sort();
82
+ }
83
+
84
+ /** One provider one lock file pins. */
85
+ export interface LockEntry {
86
+ /** The lock file, relative to the repo. */
87
+ lock: string;
88
+ address: string;
89
+ version: string;
90
+ }
91
+
92
+ /** Every provider every lock file pins. */
93
+ export function lockEntries(repo: string, files: string[] = lockFiles(repo)): LockEntry[] {
94
+ return files.flatMap((lock) => [...readLock(readFileSync(join(repo, lock), "utf-8"))].map(([address, version]) => ({ lock, address, version })));
95
+ }
96
+
97
+ /** Lock files that pin the same providers at the same versions, mirrored once. */
98
+ export interface MirrorGroup {
99
+ /** The lock files, relative to the repo. */
100
+ locks: string[];
101
+ /** The first lock file's text, which `providers mirror` checks downloads against. */
102
+ text: string;
103
+ /** Address to version. */
104
+ providers: Map<string, string>;
105
+ }
106
+
107
+ const byKey = <T>([a]: [string, T], [b]: [string, T]): number => (a < b ? -1 : a > b ? 1 : 0);
108
+
109
+ /** The repo's lock files grouped by what they pin; a lock file that pins nothing is left out. */
110
+ export function mirrorGroups(repo: string, locks: string[] = lockFiles(repo)): MirrorGroup[] {
111
+ const groups = new Map<string, MirrorGroup>();
112
+ for (const lock of locks) {
113
+ const text = readFileSync(join(repo, lock), "utf-8");
114
+ const providers = readLock(text);
115
+ if (providers.size === 0) continue;
116
+ const key = [...providers].sort(byKey).map(([a, v]) => `${a}=${v}`).join("\n");
117
+ const g = groups.get(key);
118
+ if (g) g.locks.push(lock);
119
+ else groups.set(key, { locks: [lock], text, providers });
120
+ }
121
+ return [...groups.values()];
122
+ }
123
+
124
+ /** A file a mirror build writes, relative to its directory. */
125
+ export interface MirrorModuleFile {
126
+ path: string;
127
+ content: string;
128
+ }
129
+
130
+ export interface MirrorModuleOptions {
131
+ /** Directory the modules go under. Default `mirror`. */
132
+ dir?: string;
133
+ /** Also require each provider locked on registry.terraform.io or registry.opentofu.org under the other host. Default false. */
134
+ bothHosts?: boolean;
135
+ }
136
+
137
+ /** The other default host's address for a provider locked on one of them, or undefined. */
138
+ export function otherHostAddress(address: string): string | undefined {
139
+ const [host, ...rest] = address.split("/");
140
+ if (rest.length !== 2) return undefined;
141
+ const h = host!.toLowerCase();
142
+ if (h === TERRAFORM_REGISTRY) return [OPENTOFU_REGISTRY, ...rest].join("/");
143
+ if (h === OPENTOFU_REGISTRY) return [TERRAFORM_REGISTRY, ...rest].join("/");
144
+ return undefined;
145
+ }
146
+
147
+ /**
148
+ * One module per group: `versions.tf` holding each provider at exactly its
149
+ * locked version, and the lock file beside it so `providers mirror` checks the
150
+ * hashes. With `bothHosts`, the other default host's address is required too;
151
+ * the lock file does not name it, so its download is checked by that
152
+ * registry's signature alone.
153
+ */
154
+ export function mirrorModules(groups: MirrorGroup[], options: MirrorModuleOptions = {}): MirrorModuleFile[] {
155
+ const base = options.dir ?? "mirror";
156
+ const files: MirrorModuleFile[] = [];
157
+ groups.forEach((g, i) => {
158
+ const dir = `${base}/${String(i + 1).padStart(2, "0")}`;
159
+ const wanted = new Map(g.providers);
160
+ if (options.bothHosts) {
161
+ for (const [address, version] of g.providers) {
162
+ const other = otherHostAddress(address);
163
+ if (other && ![...wanted.keys()].some((a) => a.toLowerCase() === other.toLowerCase())) wanted.set(other, version);
164
+ }
165
+ }
166
+ const required = [...wanted].sort(byKey).map(([address, version], n) => ` p${n} = { source = "${address}", version = "= ${version}" }`);
167
+ files.push({
168
+ path: `${dir}/versions.tf`,
169
+ content: [`# The providers ${g.locks.join(", ")} lock${g.locks.length === 1 ? "s" : ""}, each at exactly its locked version.`, "terraform {", " required_providers {", ...required, " }", "}", ""].join("\n"),
170
+ });
171
+ files.push({ path: `${dir}/${LOCK_FILE}`, content: g.text });
172
+ });
173
+ return files;
174
+ }
175
+
176
+ /** A platform as the binaries name it, `linux_amd64`, from that form or Docker's `linux/amd64`. */
177
+ export function binaryPlatform(platform: string): string {
178
+ const p = platform.trim().toLowerCase().replace("/", "_");
179
+ if (!/^[a-z0-9]+_[a-z0-9]+$/.test(p)) throw new Error(`${platform}: name a platform as os_arch or os/arch, such as linux_amd64`);
180
+ return p;
181
+ }
182
+
183
+ /** The platform this process runs on, as the binaries name it. */
184
+ export function hostPlatform(platform: string = process.platform, arch: string = process.arch): string {
185
+ const a = arch === "x64" ? "amd64" : arch === "ia32" ? "386" : arch;
186
+ return `${platform}_${a}`;
187
+ }
188
+
189
+ /**
190
+ * Whether a mirror directory holds the provider at the version for the
191
+ * platform, in either layout a `filesystem_mirror` reads: packed (what
192
+ * `providers mirror` writes, a zip per platform) or unpacked (a directory per
193
+ * platform).
194
+ */
195
+ export function mirrorHas(mirror: string, address: string, version: string, platform: string): boolean {
196
+ const parts = address.split("/");
197
+ const type = parts[parts.length - 1]!;
198
+ const dir = join(mirror, ...parts);
199
+ return existsSync(join(dir, `terraform-provider-${type}_${version}_${platform}.zip`)) || existsSync(join(dir, version, platform));
200
+ }
201
+
202
+ /** Runs the binary; a test passes its own. */
203
+ export type MirrorRun = (bin: string, args: string[], cwd: string) => { status: number | null; output: string };
204
+
205
+ const defaultRun: MirrorRun = (bin, args, cwd) => {
206
+ const r = spawnSync(bin, args, { cwd, encoding: "utf-8", env: { ...process.env, TF_IN_AUTOMATION: "1", TF_INPUT: "0", CHECKPOINT_DISABLE: "1" } });
207
+ return { status: r.status, output: `${r.stdout ?? ""}${r.stderr ?? ""}${r.error ? r.error.message : ""}` };
208
+ };
209
+
210
+ export interface BuildMirrorOptions {
211
+ /** The binary that runs `providers mirror`. Default tofu. */
212
+ binary?: string;
213
+ /** Platforms to mirror, `linux_amd64` or `linux/amd64`. Default linux_amd64. */
214
+ platforms?: string[];
215
+ /** Mirror each provider under both default hosts. Default true. */
216
+ bothHosts?: boolean;
217
+ /** The lock files to cover. Default every lock file in the repo. */
218
+ locks?: string[];
219
+ /** Where the modules are written while the binary runs. Default a new temporary directory. */
220
+ workDir?: string;
221
+ run?: MirrorRun;
222
+ }
223
+
224
+ export interface BuiltMirror {
225
+ /** The directory the mirror was written to. */
226
+ out: string;
227
+ /** The lock files covered. */
228
+ locks: string[];
229
+ /** Every provider mirrored, as `address version`, sorted. */
230
+ providers: string[];
231
+ platforms: string[];
232
+ }
233
+
234
+ export class MirrorBuildError extends Error {
235
+ constructor(message: string) {
236
+ super(message);
237
+ this.name = "MirrorBuildError";
238
+ }
239
+ }
240
+
241
+ /** The versions every `<version>.json` beside an `index.json` names, as the index lists them. */
242
+ const indexFor = (dir: string): string => {
243
+ const versions = readdirSync(dir)
244
+ .filter((f) => f.endsWith(".json") && f !== "index.json")
245
+ .map((f) => f.slice(0, -".json".length))
246
+ .sort();
247
+ return `${JSON.stringify({ versions: Object.fromEntries(versions.map((v) => [v, {}])) }, null, 2)}\n`;
248
+ };
249
+
250
+ /** Every provider directory (`<host>/<ns>/<type>`) under a mirror. */
251
+ const providerDirs = (out: string): string[] => {
252
+ const dirs: string[] = [];
253
+ const list = (d: string) => (existsSync(d) ? readdirSync(d).filter((n) => statSync(join(d, n)).isDirectory()) : []);
254
+ for (const host of list(out)) for (const ns of list(join(out, host))) for (const type of list(join(out, host, ns))) dirs.push(join(out, host, ns, type));
255
+ return dirs;
256
+ };
257
+
258
+ /**
259
+ * Runs `providers mirror` over every lock file's group into `out`, then
260
+ * rewrites each `index.json` from the version files present, so groups that
261
+ * mirror different versions of one provider all show.
262
+ */
263
+ export function buildMirror(repo: string, out: string, options: BuildMirrorOptions = {}): BuiltMirror {
264
+ const binary = options.binary ?? "tofu";
265
+ const platforms = [...new Set((options.platforms?.length ? options.platforms : ["linux_amd64"]).map(binaryPlatform))];
266
+ const bothHosts = options.bothHosts ?? true;
267
+ const groups = mirrorGroups(repo, options.locks ?? lockFiles(repo));
268
+ const work = options.workDir ?? mkdtempSync(join(tmpdir(), "chant-provider-mirror-"));
269
+ const run = options.run ?? defaultRun;
270
+ const modules = mirrorModules(groups, { dir: "mirror", bothHosts });
271
+ for (const f of modules) {
272
+ mkdirSync(join(work, posix.dirname(f.path)), { recursive: true });
273
+ writeFileSync(join(work, f.path), f.content);
274
+ }
275
+ mkdirSync(out, { recursive: true });
276
+ const dirs = [...new Set(modules.map((f) => posix.dirname(f.path)))];
277
+ for (const d of dirs) {
278
+ const r = run(binary, [`-chdir=${join(work, d)}`, "providers", "mirror", ...platforms.map((p) => `-platform=${p}`), out], repo);
279
+ if (r.status !== 0) throw new MirrorBuildError(`${binary} providers mirror for ${d} failed (exit ${r.status}):\n${r.output.trim()}`);
280
+ }
281
+ for (const d of providerDirs(out)) writeFileSync(join(d, "index.json"), indexFor(d));
282
+ const providers = new Set<string>();
283
+ for (const g of groups) {
284
+ for (const [address, version] of g.providers) {
285
+ providers.add(`${address} ${version}`);
286
+ const other = bothHosts ? otherHostAddress(address) : undefined;
287
+ if (other) providers.add(`${other} ${version}`);
288
+ }
289
+ }
290
+ return { out, locks: groups.flatMap((g) => g.locks).sort(), providers: [...providers].sort(), platforms };
291
+ }
292
+
293
+ /** The store a mirror is uploaded to: a registry's write store, with an optional existence check so archives already there are skipped. */
294
+ export type MirrorUploadStore = Pick<RegistryWriteStore, "put" | "read"> & { has?(key: string): Promise<boolean> };
295
+
296
+ export interface UploadMirrorOptions {
297
+ /** The registry's prefix in its store; the mirror goes under `<prefix>/providers/`. */
298
+ prefix?: string;
299
+ }
300
+
301
+ export interface UploadedMirror {
302
+ /** Keys written. */
303
+ put: string[];
304
+ /** Archive keys left as they were, already in the store. */
305
+ kept: string[];
306
+ }
307
+
308
+ const contentTypeOf = (name: string): string => (name.endsWith(".json") ? "application/json" : name.endsWith(".zip") ? "application/zip" : "application/octet-stream");
309
+
310
+ /** Every file under a directory, relative to it, `/`-separated. */
311
+ const walkFiles = (root: string): string[] => {
312
+ const out: string[] = [];
313
+ const walk = (d: string) => {
314
+ for (const n of readdirSync(d)) {
315
+ const p = join(d, n);
316
+ if (statSync(p).isDirectory()) walk(p);
317
+ else out.push(relative(root, p).split("\\").join("/"));
318
+ }
319
+ };
320
+ if (existsSync(root)) walk(root);
321
+ return out.sort();
322
+ };
323
+
324
+ /** The key a mirror file goes to. */
325
+ export const mirrorKey = (prefix: string | undefined, path: string): string => keyFor(prefix, `${PROVIDER_MIRROR_PREFIX}/${path}`);
326
+
327
+ /** The versions an `index.json` lists. */
328
+ const indexVersions = (text: string | undefined): string[] => {
329
+ if (!text) return [];
330
+ try {
331
+ return Object.keys((JSON.parse(text) as { versions?: Record<string, unknown> }).versions ?? {});
332
+ } catch {
333
+ return [];
334
+ }
335
+ };
336
+
337
+ /**
338
+ * Uploads a built mirror: archives, then each `<version>.json`, then each
339
+ * `index.json` merged with the versions the store's copy already lists, so a
340
+ * reader never sees an index naming a version whose files are not there yet.
341
+ */
342
+ export async function uploadMirror(store: MirrorUploadStore, dir: string, options: UploadMirrorOptions = {}): Promise<UploadedMirror> {
343
+ const files = walkFiles(dir);
344
+ const rank = (f: string): number => (f.endsWith("/index.json") ? 2 : f.endsWith(".json") ? 1 : 0);
345
+ const ordered = [...files].sort((a, b) => rank(a) - rank(b) || (a < b ? -1 : a > b ? 1 : 0));
346
+ const put: string[] = [];
347
+ const kept: string[] = [];
348
+ for (const f of ordered) {
349
+ const key = mirrorKey(options.prefix, f);
350
+ if (rank(f) === 0 && store.has && (await store.has(key))) {
351
+ kept.push(key);
352
+ continue;
353
+ }
354
+ let body: string | Buffer = readFileSync(join(dir, f));
355
+ if (rank(f) === 2) {
356
+ const versions = [...new Set([...indexVersions((await store.read(key)).body), ...indexVersions(body.toString("utf-8"))])].sort();
357
+ body = `${JSON.stringify({ versions: Object.fromEntries(versions.map((v) => [v, {}])) }, null, 2)}\n`;
358
+ }
359
+ await store.put(key, body, contentTypeOf(f));
360
+ put.push(key);
361
+ }
362
+ return { put, kept };
363
+ }
364
+
365
+ /** The mirror's URL under a registry's base URL, ending in `/`. */
366
+ export const mirrorUrl = (registryUrl: string, prefix?: string): string => `${registryUrl.replace(/\/+$/, "")}/${mirrorKey(prefix, "")}`;
367
+
368
+ /**
369
+ * The CLI config block users add: the mirrored providers come only from the
370
+ * mirror, and everything else directly from its registry. `include` keeps
371
+ * the mirror from being asked for a provider it lacks, and `exclude` keeps a
372
+ * mirrored provider from going direct.
373
+ */
374
+ export function mirrorCliConfig(url: string, addresses: string[]): string {
375
+ // Takes addresses, or BuiltMirror.providers' `address version` lines.
376
+ const list = [...new Set(addresses.map((a) => a.trim().split(/\s+/)[0]!))].sort();
377
+ const quoted = list.map((a) => ` "${a}",`);
378
+ return [
379
+ "provider_installation {",
380
+ " network_mirror {",
381
+ ` url = "${url.endsWith("/") ? url : `${url}/`}"`,
382
+ " include = [",
383
+ ...quoted,
384
+ " ]",
385
+ " }",
386
+ " direct {",
387
+ " exclude = [",
388
+ ...quoted,
389
+ " ]",
390
+ " }",
391
+ "}",
392
+ "",
393
+ ].join("\n");
394
+ }
395
+