@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,278 @@
1
+ /**
2
+ * The zip of an attested module archive (chant#4605, terragucci#1299 ruling
3
+ * A). Artifactory takes a module only as a zip and Nexus recommends one, so
4
+ * their targets upload the same files the bucket registry's `.tar.gz`
5
+ * carries, at the same paths with the same modes, repacked as a zip. Nothing
6
+ * is added to or dropped from the module.
7
+ *
8
+ * The repack is deterministic: entries keep the tarball's order and mtimes,
9
+ * so one archive always gives the same zip bytes. The zip's archive comment
10
+ * carries the attested content digest and the revision
11
+ * (`chant-module content=sha256:<hex> revision=<sha>`), which is how a
12
+ * target's `held` tells, from the zip a host gives back, which attested
13
+ * archive it was made from. A zip with no such comment (one uploaded by
14
+ * hand) is judged by its own digest, so it never passes for the release's.
15
+ *
16
+ * No zip64: an archive past 4 GiB or 65535 entries is refused, far above
17
+ * either host's limit (Nexus takes 100 MB per module).
18
+ */
19
+ import { deflateRawSync, gunzipSync, inflateRawSync } from "node:zlib";
20
+ import { PublishTargetError } from "@intentius/chant/lifecycle/release-publish";
21
+ const field = (b, at, len) => {
22
+ const raw = b.subarray(at, at + len);
23
+ const end = raw.indexOf(0);
24
+ return raw.subarray(0, end < 0 ? raw.length : end).toString("utf-8");
25
+ };
26
+ const octal = (b, at, len) => parseInt(field(b, at, len).trim() || "0", 8);
27
+ function paxRecords(data) {
28
+ const out = {};
29
+ let at = 0;
30
+ while (at < data.length) {
31
+ const space = data.indexOf(0x20, at);
32
+ if (space < 0)
33
+ break;
34
+ const len = parseInt(data.subarray(at, space).toString("utf-8"), 10);
35
+ if (!Number.isFinite(len) || len <= 0)
36
+ break;
37
+ const record = data.subarray(space + 1, at + len - 1).toString("utf-8");
38
+ const eq = record.indexOf("=");
39
+ if (eq > 0)
40
+ out[record.slice(0, eq)] = record.slice(eq + 1);
41
+ at += len;
42
+ }
43
+ return out;
44
+ }
45
+ const invalid = (message) => new PublishTargetError("publish-archive-invalid", message, "Publish the .tar.gz the bucket registry serves, with the module's files at its root.");
46
+ /**
47
+ * The entries of a module's gzip tarball (ustar, pax and GNU long names), with
48
+ * `./` cut from each path. A link or a device in the archive is refused: a
49
+ * zip of the module could not carry it as the tarball does.
50
+ */
51
+ export function moduleArchiveEntries(archive) {
52
+ let tar;
53
+ try {
54
+ tar = gunzipSync(archive);
55
+ }
56
+ catch {
57
+ throw invalid("the module archive is not a gzip tarball");
58
+ }
59
+ const out = [];
60
+ let at = 0;
61
+ let pax = {};
62
+ let longName;
63
+ while (at + 512 <= tar.length) {
64
+ const header = tar.subarray(at, at + 512);
65
+ if (header.every((b) => b === 0))
66
+ break;
67
+ const size = octal(header, 124, 12);
68
+ const flag = String.fromCharCode(header[156] || 0x30);
69
+ const data = tar.subarray(at + 512, at + 512 + size);
70
+ at += 512 + Math.ceil(size / 512) * 512;
71
+ if (flag === "x") {
72
+ pax = paxRecords(data);
73
+ continue;
74
+ }
75
+ if (flag === "L") {
76
+ longName = field(data, 0, data.length);
77
+ continue;
78
+ }
79
+ if (flag === "g")
80
+ continue;
81
+ const prefix = field(header, 345, 155);
82
+ const raw = pax.path ?? longName ?? (prefix ? `${prefix}/${field(header, 0, 100)}` : field(header, 0, 100));
83
+ const mtime = pax.mtime ? Math.floor(Number(pax.mtime)) : octal(header, 136, 12);
84
+ pax = {};
85
+ longName = undefined;
86
+ const path = raw.replace(/^(\.\/)+/, "").replace(/\/+$/, "");
87
+ if (!path || path === ".")
88
+ continue;
89
+ if (path.startsWith("/") || path.split("/").includes(".."))
90
+ throw invalid(`the module archive holds ${raw}, outside its root`);
91
+ const type = flag === "0" || flag === "\0" || flag === "7" ? "file" : flag === "5" ? "dir" : undefined;
92
+ if (!type)
93
+ throw invalid(`the module archive holds ${path}, which is not a regular file or a directory; a zip of the module cannot carry it`);
94
+ out.push({ path, type, mode: octal(header, 100, 8) & 0o7777, mtime, data: type === "file" ? Buffer.from(data) : Buffer.alloc(0) });
95
+ }
96
+ return out;
97
+ }
98
+ const CRC_TABLE = (() => {
99
+ const t = new Uint32Array(256);
100
+ for (let n = 0; n < 256; n++) {
101
+ let c = n;
102
+ for (let k = 0; k < 8; k++)
103
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
104
+ t[n] = c >>> 0;
105
+ }
106
+ return t;
107
+ })();
108
+ /** The CRC-32 a zip entry carries. */
109
+ export function crc32(data) {
110
+ let c = 0xffffffff;
111
+ for (let i = 0; i < data.length; i++)
112
+ c = CRC_TABLE[(c ^ data[i]) & 0xff] ^ (c >>> 8);
113
+ return (c ^ 0xffffffff) >>> 0;
114
+ }
115
+ /** MS-DOS date and time, as a zip entry keeps them, in UTC and clamped to 1980. */
116
+ function dosTime(seconds) {
117
+ const d = new Date(Math.max(seconds * 1000, Date.UTC(1980, 0, 1)));
118
+ return {
119
+ time: (d.getUTCHours() << 11) | (d.getUTCMinutes() << 5) | Math.floor(d.getUTCSeconds() / 2),
120
+ date: ((d.getUTCFullYear() - 1980) << 9) | ((d.getUTCMonth() + 1) << 5) | d.getUTCDate(),
121
+ };
122
+ }
123
+ /** The archive comment that names the attested archive a zip was made from. */
124
+ export const ZIP_COMMENT_PREFIX = "chant-module";
125
+ export const moduleZipComment = (content, revision) => `${ZIP_COMMENT_PREFIX} content=${content}${revision ? ` revision=${revision}` : ""}`;
126
+ /** The content and revision a zip's archive comment names, when it is one {@link moduleZipComment} wrote. */
127
+ export function readModuleZipComment(comment) {
128
+ if (!comment.startsWith(`${ZIP_COMMENT_PREFIX} `))
129
+ return undefined;
130
+ const content = /(?:^| )content=(sha256:[0-9a-f]{64})(?: |$)/.exec(comment)?.[1];
131
+ if (!content)
132
+ return undefined;
133
+ const revision = /(?:^| )revision=([^ ]+)(?: |$)/.exec(comment)?.[1];
134
+ return { content, ...(revision ? { revision } : {}) };
135
+ }
136
+ /** A zip of the given entries, with `comment` as its archive comment. Files are deflated. */
137
+ export function zipOf(entries, comment = "") {
138
+ if (entries.length > 0xffff)
139
+ throw invalid(`the module archive holds ${entries.length} entries; a zip without zip64 holds 65535`);
140
+ const locals = [];
141
+ const centrals = [];
142
+ let offset = 0;
143
+ for (const e of entries) {
144
+ const name = Buffer.from(e.type === "dir" ? `${e.path}/` : e.path, "utf-8");
145
+ const data = e.type === "file" ? e.data : Buffer.alloc(0);
146
+ const packed = e.type === "file" ? deflateRawSync(data, { level: 9 }) : Buffer.alloc(0);
147
+ const method = e.type === "file" ? 8 : 0;
148
+ const crc = crc32(data);
149
+ const { time, date } = dosTime(e.mtime);
150
+ if (packed.length > 0xfffffffe || data.length > 0xfffffffe || offset > 0xfffffffe)
151
+ throw invalid("the module archive is past 4 GiB; a zip without zip64 holds less");
152
+ const local = Buffer.alloc(30);
153
+ local.writeUInt32LE(0x04034b50, 0);
154
+ local.writeUInt16LE(20, 4);
155
+ local.writeUInt16LE(0x0800, 6);
156
+ local.writeUInt16LE(method, 8);
157
+ local.writeUInt16LE(time, 10);
158
+ local.writeUInt16LE(date, 12);
159
+ local.writeUInt32LE(crc, 14);
160
+ local.writeUInt32LE(packed.length, 18);
161
+ local.writeUInt32LE(data.length, 22);
162
+ local.writeUInt16LE(name.length, 26);
163
+ local.writeUInt16LE(0, 28);
164
+ locals.push(local, name, packed);
165
+ const unixMode = (e.type === "dir" ? 0o040000 : 0o100000) | (e.mode & 0o7777 || (e.type === "dir" ? 0o755 : 0o644));
166
+ const central = Buffer.alloc(46);
167
+ central.writeUInt32LE(0x02014b50, 0);
168
+ central.writeUInt16LE((3 << 8) | 20, 4);
169
+ central.writeUInt16LE(20, 6);
170
+ central.writeUInt16LE(0x0800, 8);
171
+ central.writeUInt16LE(method, 10);
172
+ central.writeUInt16LE(time, 12);
173
+ central.writeUInt16LE(date, 14);
174
+ central.writeUInt32LE(crc, 16);
175
+ central.writeUInt32LE(packed.length, 20);
176
+ central.writeUInt32LE(data.length, 24);
177
+ central.writeUInt16LE(name.length, 28);
178
+ central.writeUInt16LE(0, 30);
179
+ central.writeUInt16LE(0, 32);
180
+ central.writeUInt16LE(0, 34);
181
+ central.writeUInt16LE(0, 36);
182
+ central.writeUInt32LE(((unixMode << 16) | (e.type === "dir" ? 0x10 : 0)) >>> 0, 38);
183
+ central.writeUInt32LE(offset, 42);
184
+ centrals.push(central, name);
185
+ offset += local.length + name.length + packed.length;
186
+ }
187
+ const cd = Buffer.concat(centrals);
188
+ const note = Buffer.from(comment, "utf-8");
189
+ if (note.length > 0xffff)
190
+ throw invalid("the zip comment is longer than a zip carries");
191
+ const end = Buffer.alloc(22);
192
+ end.writeUInt32LE(0x06054b50, 0);
193
+ end.writeUInt16LE(0, 4);
194
+ end.writeUInt16LE(0, 6);
195
+ end.writeUInt16LE(entries.length, 8);
196
+ end.writeUInt16LE(entries.length, 10);
197
+ end.writeUInt32LE(cd.length, 12);
198
+ end.writeUInt32LE(offset, 16);
199
+ end.writeUInt16LE(note.length, 20);
200
+ return Buffer.concat([...locals, cd, end, note]);
201
+ }
202
+ /**
203
+ * The zip a hosted target uploads for an attested archive: its entries,
204
+ * repacked, with the content digest and revision in the archive comment.
205
+ */
206
+ export function moduleZip(archive, content, revision) {
207
+ return zipOf(moduleArchiveEntries(archive), moduleZipComment(content, revision));
208
+ }
209
+ /** Reads a zip's central directory and entries. Throws on bytes that are not a zip. */
210
+ export function readZip(bytes) {
211
+ const b = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
212
+ let end = -1;
213
+ for (let i = b.length - 22; i >= Math.max(0, b.length - 22 - 0xffff); i--) {
214
+ if (b.readUInt32LE(i) === 0x06054b50) {
215
+ end = i;
216
+ break;
217
+ }
218
+ }
219
+ if (end < 0)
220
+ throw new Error("not a zip archive: no end of central directory");
221
+ const count = b.readUInt16LE(end + 10);
222
+ const cdOffset = b.readUInt32LE(end + 16);
223
+ const comment = b.subarray(end + 22, end + 22 + b.readUInt16LE(end + 20)).toString("utf-8");
224
+ const entries = [];
225
+ let at = cdOffset;
226
+ for (let n = 0; n < count; n++) {
227
+ if (b.readUInt32LE(at) !== 0x02014b50)
228
+ throw new Error("not a zip archive: a bad central directory entry");
229
+ const method = b.readUInt16LE(at + 10);
230
+ const csize = b.readUInt32LE(at + 20);
231
+ const nameLen = b.readUInt16LE(at + 28);
232
+ const extraLen = b.readUInt16LE(at + 30);
233
+ const commentLen = b.readUInt16LE(at + 32);
234
+ const external = b.readUInt32LE(at + 38);
235
+ const local = b.readUInt32LE(at + 42);
236
+ const name = b.subarray(at + 46, at + 46 + nameLen).toString("utf-8");
237
+ at += 46 + nameLen + extraLen + commentLen;
238
+ if (b.readUInt32LE(local) !== 0x04034b50)
239
+ throw new Error(`not a zip archive: no local header for ${name}`);
240
+ const start = local + 30 + b.readUInt16LE(local + 26) + b.readUInt16LE(local + 28);
241
+ const packed = b.subarray(start, start + csize);
242
+ const data = method === 0 ? Buffer.from(packed) : method === 8 ? inflateRawSync(packed) : undefined;
243
+ if (!data)
244
+ throw new Error(`${name}: zip method ${method} is not read here`);
245
+ const dir = name.endsWith("/");
246
+ entries.push({ path: dir ? name.slice(0, -1) : name, dir, mode: (external >>> 16) & 0o7777, data });
247
+ }
248
+ return { comment, entries };
249
+ }
250
+ const isGzip = (b) => b.length >= 2 && b[0] === 0x1f && b[1] === 0x8b;
251
+ const isZip = (b) => b.length >= 4 && b[0] === 0x50 && b[1] === 0x4b;
252
+ /**
253
+ * The content a module archive a host gave back carries, as `held` reports
254
+ * it: the attested digest a chant zip's comment names; for a gzip tarball,
255
+ * the digest of its uncompressed bytes, as the bucket registry keeps it; for
256
+ * any other bytes, their own digest.
257
+ */
258
+ export function heldContentOf(bytes, digest) {
259
+ if (isZip(bytes)) {
260
+ try {
261
+ const named = readModuleZipComment(readZip(bytes).comment);
262
+ if (named)
263
+ return named;
264
+ }
265
+ catch {
266
+ // Not a zip after all: judged by its own digest below.
267
+ }
268
+ }
269
+ else if (isGzip(bytes)) {
270
+ try {
271
+ return { content: digest(gunzipSync(bytes)) };
272
+ }
273
+ catch {
274
+ // A broken gzip: judged by its own digest below.
275
+ }
276
+ }
277
+ return { content: digest(bytes) };
278
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The module registry protocol, written as files a bucket, a directory or a
3
+ * Pages site holds (https://opentofu.org/docs/internals/module-registry-protocol/):
4
+ *
5
+ * .well-known/terraform.json {"modules.v1": "/v1/modules/"}
6
+ * v1/modules/<ns>/<name>/<system>/versions every version, oldest first
7
+ * v1/modules/<ns>/<name>/<system>/<v>/download {"location": ...}
8
+ * v1/modules/<ns>/<name>/<system>/<v>/<name>-<v>.tar.gz a tarball download
9
+ * v1/modules/<ns>/<name>/<system>/<v>/release.json the commit and content digest
10
+ *
11
+ * The download answer the two CLIs need is a 200 with the JSON body and an
12
+ * `X-Terraform-Get` header naming the same location (terragucci#1287).
13
+ * Terraform reads only the header (hashicorp/terraform
14
+ * internal/registry/client.go:237-250 at v1.14.9 and v1.15.8). OpenTofu reads
15
+ * the body's `location` first and the header when it is empty
16
+ * (opentofu/opentofu internal/registry/client.go:205-237 at v1.12.5 and
17
+ * v1.13.1). A stored file carries no header, so the stored `download` file is
18
+ * the body, and whatever serves it (`downloadAnswer`, the loopback handler in
19
+ * ./serve, an edge function) adds the header from that body.
20
+ *
21
+ * The writer takes the store it writes to, so the bucket clients stay with
22
+ * whoever owns them (chant#4554 moves terragucci's into core).
23
+ */
24
+ import { type StoreCondition } from "@intentius/chant/object-store/errors";
25
+ import { type RegistryAddress } from "./address.js";
26
+ import { type Semver } from "./semver.js";
27
+ /** The header Terraform takes a module's location from. */
28
+ export declare const DOWNLOAD_HEADER = "X-Terraform-Get";
29
+ /** The files a registry is written to: a bucket client, a directory, or a test's map. */
30
+ export interface RegistryWriteStore {
31
+ /** Writes one object. With `when`, the write must fail with a conflict when the condition no longer holds. */
32
+ put(key: string, body: string | Buffer, contentType?: string, when?: StoreCondition): Promise<unknown>;
33
+ /** One object's body and version tag, or `{}` when there is none. */
34
+ read(key: string): Promise<{
35
+ body?: string;
36
+ etag?: string;
37
+ }>;
38
+ }
39
+ /** The service discovery document. */
40
+ export declare const discoveryDoc: () => string;
41
+ /** The protocol's versions answer, oldest first, duplicates and non-versions dropped. */
42
+ export declare function versionsDoc(versions: string[]): string;
43
+ /** The versions a versions answer lists, or none when it is missing or not the protocol's JSON. */
44
+ export declare function readVersions(text: string | undefined): string[];
45
+ /** A version's download answer body: the JSON OpenTofu reads. */
46
+ export declare function downloadDoc(location: string): string;
47
+ /** The location a download body names, or undefined when it names none. */
48
+ export declare function downloadLocation(body: string | undefined): string | undefined;
49
+ /** A download answer as a server sends it. */
50
+ export interface DownloadAnswer {
51
+ status: 200;
52
+ headers: Record<string, string>;
53
+ body: string;
54
+ }
55
+ /**
56
+ * The download answer both CLIs install from: status 200, the JSON body with
57
+ * `location`, and `X-Terraform-Get` naming the same location.
58
+ */
59
+ export declare function downloadAnswer(location: string): DownloadAnswer;
60
+ /** The headers a stored download body is served with, or undefined when the body names no location. */
61
+ export declare function downloadHeaders(body: string | undefined): Record<string, string> | undefined;
62
+ /** True for a `.../v1/modules/<ns>/<name>/<system>/<version>/download` path, with or without a leading prefix. */
63
+ export declare const isDownloadPath: (path: string) => boolean;
64
+ /** What the registry keeps beside a version, so the next publish knows what it holds. */
65
+ export interface ReleaseMeta {
66
+ module: string;
67
+ version: string;
68
+ revision: string;
69
+ content: string;
70
+ location: string;
71
+ }
72
+ /** The registry's files for one version, written in this order: the version is listed last, once all it points at is there. */
73
+ export interface VersionFiles {
74
+ tarball?: {
75
+ key: string;
76
+ body: Buffer;
77
+ };
78
+ download: {
79
+ key: string;
80
+ body: string;
81
+ };
82
+ meta: {
83
+ key: string;
84
+ body: string;
85
+ };
86
+ }
87
+ export interface WriteVersionOptions {
88
+ /** The registry's prefix in its store. */
89
+ prefix?: string;
90
+ /**
91
+ * Whether an error from `put` is a lost conditional write, to be retried.
92
+ * By default chant's `StoreConflict`, or any error named `StoreConflict`
93
+ * (a store from another package with a class of its own).
94
+ */
95
+ isConflict?: (e: unknown) => boolean;
96
+ /** Conditional retries on the versions file before the conflict is thrown. Default 3. */
97
+ retries?: number;
98
+ }
99
+ /** Writes one version: discovery when it differs, the tarball, the download answer, release.json, then the versions list. */
100
+ export declare function writeVersion(store: RegistryWriteStore, addr: RegistryAddress, version: Semver, files: VersionFiles, options?: WriteVersionOptions): Promise<void>;
101
+ //# sourceMappingURL=writer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../src/registry/writer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The module registry protocol, written as files a bucket, a directory or a
3
+ * Pages site holds (https://opentofu.org/docs/internals/module-registry-protocol/):
4
+ *
5
+ * .well-known/terraform.json {"modules.v1": "/v1/modules/"}
6
+ * v1/modules/<ns>/<name>/<system>/versions every version, oldest first
7
+ * v1/modules/<ns>/<name>/<system>/<v>/download {"location": ...}
8
+ * v1/modules/<ns>/<name>/<system>/<v>/<name>-<v>.tar.gz a tarball download
9
+ * v1/modules/<ns>/<name>/<system>/<v>/release.json the commit and content digest
10
+ *
11
+ * The download answer the two CLIs need is a 200 with the JSON body and an
12
+ * `X-Terraform-Get` header naming the same location (terragucci#1287).
13
+ * Terraform reads only the header (hashicorp/terraform
14
+ * internal/registry/client.go:237-250 at v1.14.9 and v1.15.8). OpenTofu reads
15
+ * the body's `location` first and the header when it is empty
16
+ * (opentofu/opentofu internal/registry/client.go:205-237 at v1.12.5 and
17
+ * v1.13.1). A stored file carries no header, so the stored `download` file is
18
+ * the body, and whatever serves it (`downloadAnswer`, the loopback handler in
19
+ * ./serve, an edge function) adds the header from that body.
20
+ *
21
+ * The writer takes the store it writes to, so the bucket clients stay with
22
+ * whoever owns them (chant#4554 moves terragucci's into core).
23
+ */
24
+ // The errors module alone, never the object-store index: its clients import Node built-ins, and the
25
+ // edge function (./edge) reaches this file for the download answer (chant#4589).
26
+ import { isStoreConflict } from "@intentius/chant/object-store/errors";
27
+ import { DISCOVERY, MODULES_V1, keyFor } from "./address.js";
28
+ import { compareSemver, formatSemver, parseSemver } from "./semver.js";
29
+ /** The header Terraform takes a module's location from. */
30
+ export const DOWNLOAD_HEADER = "X-Terraform-Get";
31
+ /** The service discovery document. */
32
+ export const discoveryDoc = () => `${JSON.stringify({ "modules.v1": MODULES_V1 })}\n`;
33
+ /** The protocol's versions answer, oldest first, duplicates and non-versions dropped. */
34
+ export function versionsDoc(versions) {
35
+ // Dedupe after formatting, so "v1.0.0" and "1.0.0" are one version.
36
+ const parsed = versions.map((v) => parseSemver(v)).filter((v) => !!v);
37
+ const sorted = [...new Set(parsed.sort(compareSemver).map(formatSemver))];
38
+ return `${JSON.stringify({ modules: [{ versions: sorted.map((version) => ({ version })) }] }, null, 2)}\n`;
39
+ }
40
+ /** The versions a versions answer lists, or none when it is missing or not the protocol's JSON. */
41
+ export function readVersions(text) {
42
+ if (!text)
43
+ return [];
44
+ try {
45
+ const doc = JSON.parse(text);
46
+ return (doc.modules?.[0]?.versions ?? []).map((v) => v.version).filter((v) => typeof v === "string");
47
+ }
48
+ catch {
49
+ return [];
50
+ }
51
+ }
52
+ /** A version's download answer body: the JSON OpenTofu reads. */
53
+ export function downloadDoc(location) {
54
+ return `${JSON.stringify({ location })}\n`;
55
+ }
56
+ /** The location a download body names, or undefined when it names none. */
57
+ export function downloadLocation(body) {
58
+ if (!body)
59
+ return undefined;
60
+ try {
61
+ const location = JSON.parse(body).location;
62
+ return typeof location === "string" && location ? location : undefined;
63
+ }
64
+ catch {
65
+ return undefined;
66
+ }
67
+ }
68
+ /**
69
+ * The download answer both CLIs install from: status 200, the JSON body with
70
+ * `location`, and `X-Terraform-Get` naming the same location.
71
+ */
72
+ export function downloadAnswer(location) {
73
+ return { status: 200, headers: { "content-type": "application/json", [DOWNLOAD_HEADER]: location }, body: downloadDoc(location) };
74
+ }
75
+ /** The headers a stored download body is served with, or undefined when the body names no location. */
76
+ export function downloadHeaders(body) {
77
+ const location = downloadLocation(body);
78
+ return location ? downloadAnswer(location).headers : undefined;
79
+ }
80
+ /** True for a `.../v1/modules/<ns>/<name>/<system>/<version>/download` path, with or without a leading prefix. */
81
+ export const isDownloadPath = (path) => /(^|\/)v1\/modules\/[^/]+\/[^/]+\/[^/]+\/[^/]+\/download$/.test(path.split("?")[0]);
82
+ /** Writes one version: discovery when it differs, the tarball, the download answer, release.json, then the versions list. */
83
+ export async function writeVersion(store, addr, version, files, options = {}) {
84
+ const discovery = discoveryDoc();
85
+ const discoKey = keyFor(options.prefix, DISCOVERY);
86
+ const disco = await store.read(discoKey);
87
+ if (disco.body !== discovery)
88
+ await store.put(discoKey, discovery, "application/json");
89
+ if (files.tarball)
90
+ await store.put(files.tarball.key, files.tarball.body, "application/gzip");
91
+ await store.put(files.download.key, files.download.body, "application/json");
92
+ await store.put(files.meta.key, files.meta.body, "application/json");
93
+ const key = keyFor(options.prefix, `${addr.path}/versions`);
94
+ const isConflict = options.isConflict ?? isStoreConflict;
95
+ const retries = options.retries ?? 3;
96
+ // Read, add, write only over the copy read: two publishes at once cannot drop each other's version.
97
+ for (let attempt = 0;; attempt++) {
98
+ const held = await store.read(key);
99
+ const when = held.body === undefined ? { ifNoneMatch: "*" } : held.etag ? { ifMatch: held.etag } : undefined;
100
+ try {
101
+ await store.put(key, versionsDoc([...readVersions(held.body), formatSemver(version)]), "application/json", when);
102
+ return;
103
+ }
104
+ catch (e) {
105
+ if (!isConflict(e) || attempt >= retries)
106
+ throw e;
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The record store a live root resolves to, read from its HCL the way
3
+ * choudoufu resolves it (INTENTIUS/choudoufu#2293, `internal/configs/live_derive.go`
4
+ * at choudoufu 74ffde956bfd97f44b8ad444424584e23321adc2). Shared by TF024,
5
+ * which names where records go, and TF046, which guards a CI root that ends
6
+ * up on the local store, so the two never disagree.
7
+ *
8
+ * The order is choudoufu's:
9
+ *
10
+ * 1. A `record_store "<kind>"` block in the `live` block or the
11
+ * `estate.chdf.hcl` sidecar always wins (`source: "record_store"`).
12
+ * 2. Otherwise the backend block choudoufu ignores under live gives the store,
13
+ * s3 to s3, gcs to gcs, azurerm to azurerm (`source: "backend"`). s3 and gcs
14
+ * need a literal `bucket`; azurerm needs literal `storage_account_name`,
15
+ * `container_name` and `resource_group_name`. A value passed only with
16
+ * `-backend-config` is not in the configuration, so choudoufu does not see
17
+ * it either.
18
+ * 3. Otherwise the store is local (`source: "implied"`), with `note` saying
19
+ * why a backend that was there was not used.
20
+ *
21
+ * This mirrors `record_store.source` and `record_store.note` in
22
+ * `choudoufu live-check -json`. A literal here is a string hcl2json hands back
23
+ * with no `${...}` in it, the same "not determined" reading the Terragrunt
24
+ * rules use (`./terragrunt.ts`).
25
+ */
26
+
27
+ import type { BlockBody } from "../../hcl/parse";
28
+ import { backendTypes } from "./tf001";
29
+ import { isExpression } from "./terragrunt";
30
+
31
+ export type RecordStoreSource = "record_store" | "backend" | "implied";
32
+
33
+ export interface ResolvedRecordStore {
34
+ /** `local`, `s3`, `gcs`, `azurerm` or `kubernetes`, or whatever label a `record_store` block carries. */
35
+ kind: string;
36
+ source: RecordStoreSource;
37
+ /** The backend type the store came from, or was not derived from, when there is a backend block. */
38
+ backend?: string;
39
+ /** Why a backend block that was present did not give a store. Set only with `source: "implied"`. */
40
+ note?: string;
41
+ }
42
+
43
+ /** Backend types choudoufu derives a record store from (INTENTIUS/choudoufu#2293). */
44
+ export const DERIVABLE_BACKENDS: ReadonlySet<string> = new Set(["s3", "gcs", "azurerm"]);
45
+
46
+ const asRecord = (v: unknown): BlockBody =>
47
+ typeof v === "object" && v !== null && !Array.isArray(v) ? (v as BlockBody) : {};
48
+
49
+ const firstBody = (v: unknown): BlockBody => asRecord(Array.isArray(v) ? v[0] : v);
50
+
51
+ /**
52
+ * The label of a `record_store "<kind>"` block in a live declaration body, or
53
+ * undefined when it declares none. hcl2json nests the label one level down,
54
+ * as it does for `backend` (see `backendTypes` in `./tf001.ts`).
55
+ */
56
+ export function recordStoreKind(liveBody: BlockBody): string | undefined {
57
+ const raw = liveBody["record_store"];
58
+ if (raw === undefined || raw === null) return undefined;
59
+ if (typeof raw !== "object" || Array.isArray(raw)) return undefined;
60
+ const labels = Object.keys(raw as BlockBody);
61
+ return labels[0];
62
+ }
63
+
64
+ /** A top-level backend argument as a literal string, or undefined when it is absent, empty or an expression. */
65
+ function literal(body: BlockBody, name: string): string | undefined {
66
+ const v = body[name];
67
+ if (typeof v !== "string" || v === "" || isExpression(v)) return undefined;
68
+ return v;
69
+ }
70
+
71
+ /**
72
+ * Why a backend block gives no record store, or undefined when it gives one.
73
+ * The wording follows choudoufu's `Live.RecordStoreNote`; an empty string
74
+ * means no store and nothing to say (a `local` backend).
75
+ */
76
+ function derivationNote(type: string, body: BlockBody): string | undefined {
77
+ const where = `backend "${type}"`;
78
+ switch (type) {
79
+ case "s3":
80
+ case "gcs":
81
+ return literal(body, "bucket") === undefined
82
+ ? `${where} sets no literal bucket; values passed with -backend-config are not read`
83
+ : undefined;
84
+ case "azurerm":
85
+ for (const name of ["storage_account_name", "container_name", "resource_group_name"]) {
86
+ if (literal(body, name) === undefined) return `${where} sets no literal ${name}, which the azurerm record store needs`;
87
+ }
88
+ return undefined;
89
+ case "local":
90
+ // choudoufu leaves no note here: a local backend and the local store agree.
91
+ return "";
92
+ default:
93
+ return `${where} has no record store counterpart`;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Resolve one live root's record store.
99
+ *
100
+ * `terraformBodies` are the bodies of every `terraform` block in the root (a
101
+ * backend can sit in a separate `backend.tf`). `liveBodies` are the root's
102
+ * live declarations: its `live` block and its sidecar, if any. choudoufu
103
+ * refuses a root that has both, so the first `record_store` found is the one.
104
+ */
105
+ export function resolveRecordStore(
106
+ terraformBodies: readonly BlockBody[],
107
+ liveBodies: readonly BlockBody[],
108
+ ): ResolvedRecordStore {
109
+ for (const live of liveBodies) {
110
+ const kind = recordStoreKind(live);
111
+ if (kind !== undefined) return { kind, source: "record_store" };
112
+ }
113
+ for (const tf of terraformBodies) {
114
+ const type = backendTypes(tf)[0];
115
+ if (type === undefined) continue;
116
+ const body = firstBody(asRecord(tf["backend"])[type]);
117
+ const note = derivationNote(type, body);
118
+ if (note === undefined) return { kind: type, source: "backend", backend: type };
119
+ return { kind: "local", source: "implied", backend: type, ...(note ? { note } : {}) };
120
+ }
121
+ return { kind: "local", source: "implied" };
122
+ }
@@ -30,8 +30,8 @@
30
30
  * module's `terraform` block is skipped here rather than read as if it were
31
31
  * the root's.
32
32
  *
33
- * Does not fire on a live root (#2103): a `backend` block is exactly what
34
- * choudoufu refuses there (TF024's territory), and the fallback to local
33
+ * Does not fire on a live root (#2103): choudoufu ignores a `backend` block
34
+ * there (TF024 reports what it still does with it), and the fallback to local
35
35
  * state this check warns about does not apply, since a live root keeps no
36
36
  * state file at all, local or remote.
37
37
  */