@intentius/chant-lexicon-terraform 0.57.0 → 0.59.0
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.
- package/README.md +9 -0
- package/dist/composites/terraform-adopt-op.d.ts +131 -0
- package/dist/composites/terraform-adopt-op.d.ts.map +1 -0
- package/dist/composites/terraform-apply-op.d.ts +70 -17
- package/dist/composites/terraform-apply-op.d.ts.map +1 -1
- package/dist/composites/terraform-watch-op.d.ts +104 -37
- package/dist/composites/terraform-watch-op.d.ts.map +1 -1
- package/dist/config.d.ts +49 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/describe-resources.d.ts +256 -16
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/hcl/descend.d.ts +98 -0
- package/dist/hcl/descend.d.ts.map +1 -0
- package/dist/hcl/marker.d.ts +130 -0
- package/dist/hcl/marker.d.ts.map +1 -0
- package/dist/hcl/parse.d.ts +156 -9
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/references.d.ts +76 -0
- package/dist/hcl/references.d.ts.map +1 -0
- package/dist/hcl/roots.d.ts +22 -3
- package/dist/hcl/roots.d.ts.map +1 -1
- package/dist/hcl/suppressions.d.ts +54 -0
- package/dist/hcl/suppressions.d.ts.map +1 -0
- package/dist/hcl/value.d.ts +77 -0
- package/dist/hcl/value.d.ts.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +34 -5
- package/dist/lint/audit-catalog.d.ts +3 -2
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +95 -0
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/blocks.d.ts +53 -0
- package/dist/lint/post-synth/blocks.d.ts.map +1 -0
- package/dist/lint/post-synth/fixtures/load.d.ts +37 -0
- package/dist/lint/post-synth/fixtures/load.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/module-source.d.ts +38 -0
- package/dist/lint/post-synth/module-source.d.ts.map +1 -0
- package/dist/lint/post-synth/scope.d.ts +65 -0
- package/dist/lint/post-synth/scope.d.ts.map +1 -0
- package/dist/lint/post-synth/tf001.d.ts +58 -6
- package/dist/lint/post-synth/tf001.d.ts.map +1 -1
- package/dist/lint/post-synth/tf002.d.ts +32 -0
- package/dist/lint/post-synth/tf002.d.ts.map +1 -0
- package/dist/lint/post-synth/tf003.d.ts +22 -0
- package/dist/lint/post-synth/tf003.d.ts.map +1 -0
- package/dist/lint/post-synth/tf004.d.ts +22 -0
- package/dist/lint/post-synth/tf004.d.ts.map +1 -0
- package/dist/lint/post-synth/tf005.d.ts +28 -0
- package/dist/lint/post-synth/tf005.d.ts.map +1 -0
- package/dist/lint/post-synth/tf006.d.ts +21 -0
- package/dist/lint/post-synth/tf006.d.ts.map +1 -0
- package/dist/lint/post-synth/tf007.d.ts +21 -0
- package/dist/lint/post-synth/tf007.d.ts.map +1 -0
- package/dist/lint/post-synth/tf008.d.ts +22 -0
- package/dist/lint/post-synth/tf008.d.ts.map +1 -0
- package/dist/lint/post-synth/tf009.d.ts +24 -0
- package/dist/lint/post-synth/tf009.d.ts.map +1 -0
- package/dist/lint/post-synth/tf010.d.ts +19 -0
- package/dist/lint/post-synth/tf010.d.ts.map +1 -0
- package/dist/lint/post-synth/tf011.d.ts +18 -0
- package/dist/lint/post-synth/tf011.d.ts.map +1 -0
- package/dist/lint/post-synth/tf012.d.ts +17 -0
- package/dist/lint/post-synth/tf012.d.ts.map +1 -0
- package/dist/lint/post-synth/tf013.d.ts +21 -0
- package/dist/lint/post-synth/tf013.d.ts.map +1 -0
- package/dist/lint/post-synth/tf014.d.ts +25 -0
- package/dist/lint/post-synth/tf014.d.ts.map +1 -0
- package/dist/lint/post-synth/tf015.d.ts +30 -0
- package/dist/lint/post-synth/tf015.d.ts.map +1 -0
- package/dist/lint/post-synth/tf016.d.ts +30 -0
- package/dist/lint/post-synth/tf016.d.ts.map +1 -0
- package/dist/lint/post-synth/tf017.d.ts +19 -0
- package/dist/lint/post-synth/tf017.d.ts.map +1 -0
- package/dist/lint/post-synth/tf018.d.ts +20 -0
- package/dist/lint/post-synth/tf018.d.ts.map +1 -0
- package/dist/lint/post-synth/tf019.d.ts +25 -0
- package/dist/lint/post-synth/tf019.d.ts.map +1 -0
- package/dist/lint/post-synth/tf020.d.ts +38 -0
- package/dist/lint/post-synth/tf020.d.ts.map +1 -0
- package/dist/lint/post-synth/tf021.d.ts +41 -0
- package/dist/lint/post-synth/tf021.d.ts.map +1 -0
- package/dist/lint/post-synth/tf022.d.ts +24 -0
- package/dist/lint/post-synth/tf022.d.ts.map +1 -0
- package/dist/lint/post-synth/tf024.d.ts +24 -0
- package/dist/lint/post-synth/tf024.d.ts.map +1 -0
- package/dist/lint/post-synth/tf025.d.ts +29 -0
- package/dist/lint/post-synth/tf025.d.ts.map +1 -0
- package/dist/lint/post-synth/tf026.d.ts +28 -0
- package/dist/lint/post-synth/tf026.d.ts.map +1 -0
- package/dist/lint/post-synth/tf027.d.ts +30 -0
- package/dist/lint/post-synth/tf027.d.ts.map +1 -0
- package/dist/lint/post-synth/tf028.d.ts +33 -0
- package/dist/lint/post-synth/tf028.d.ts.map +1 -0
- package/dist/lint/rules/fixtures/load.d.ts +13 -0
- package/dist/lint/rules/fixtures/load.d.ts.map +1 -0
- package/dist/lint/rules/plan-before-apply.d.ts +11 -0
- package/dist/lint/rules/plan-before-apply.d.ts.map +1 -1
- package/dist/lint/secret-shape.d.ts +68 -0
- package/dist/lint/secret-shape.d.ts.map +1 -0
- package/dist/live-ownership.d.ts +55 -0
- package/dist/live-ownership.d.ts.map +1 -0
- package/dist/lsp/option-keys.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +26 -0
- package/dist/okf/rules/TF002.md +11 -0
- package/dist/okf/rules/TF003.md +11 -0
- package/dist/okf/rules/TF004.md +11 -0
- package/dist/okf/rules/TF005.md +11 -0
- package/dist/okf/rules/TF006.md +11 -0
- package/dist/okf/rules/TF007.md +11 -0
- package/dist/okf/rules/TF008.md +11 -0
- package/dist/okf/rules/TF009.md +11 -0
- package/dist/okf/rules/TF010.md +11 -0
- package/dist/okf/rules/TF011.md +11 -0
- package/dist/okf/rules/TF012.md +11 -0
- package/dist/okf/rules/TF013.md +11 -0
- package/dist/okf/rules/TF014.md +11 -0
- package/dist/okf/rules/TF015.md +11 -0
- package/dist/okf/rules/TF016.md +11 -0
- package/dist/okf/rules/TF017.md +11 -0
- package/dist/okf/rules/TF018.md +11 -0
- package/dist/okf/rules/TF019.md +11 -0
- package/dist/okf/rules/TF020.md +11 -0
- package/dist/okf/rules/TF021.md +11 -0
- package/dist/okf/rules/TF022.md +11 -0
- package/dist/okf/rules/TF024.md +11 -0
- package/dist/okf/rules/TF025.md +11 -0
- package/dist/okf/rules/TF026.md +11 -0
- package/dist/okf/rules/TF027.md +11 -0
- package/dist/okf/rules/TF028.md +11 -0
- package/dist/op/activities/index.d.ts +13 -6
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/live-detect.d.ts +51 -0
- package/dist/op/activities/live-detect.d.ts.map +1 -0
- package/dist/op/activities/terraform.d.ts +466 -17
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +148 -0
- package/dist/op/activity-contracts.d.ts.map +1 -0
- package/dist/op/adoption.d.ts +118 -0
- package/dist/op/adoption.d.ts.map +1 -0
- package/dist/op/builders.d.ts +48 -6
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/resolve-root-mode.d.ts +59 -0
- package/dist/op/resolve-root-mode.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/blocks.ts +109 -0
- package/dist/rules/module-source.ts +121 -0
- package/dist/rules/plan-before-apply.ts +11 -0
- package/dist/rules/scope.ts +107 -0
- package/dist/rules/tf001.ts +98 -18
- package/dist/rules/tf002.ts +151 -0
- package/dist/rules/tf003.ts +71 -0
- package/dist/rules/tf004.ts +63 -0
- package/dist/rules/tf005.ts +76 -0
- package/dist/rules/tf006.ts +53 -0
- package/dist/rules/tf007.ts +78 -0
- package/dist/rules/tf008.ts +75 -0
- package/dist/rules/tf009.ts +59 -0
- package/dist/rules/tf010.ts +51 -0
- package/dist/rules/tf011.ts +49 -0
- package/dist/rules/tf012.ts +47 -0
- package/dist/rules/tf013.ts +63 -0
- package/dist/rules/tf014.ts +72 -0
- package/dist/rules/tf015.ts +72 -0
- package/dist/rules/tf016.ts +75 -0
- package/dist/rules/tf017.ts +50 -0
- package/dist/rules/tf018.ts +72 -0
- package/dist/rules/tf019.ts +72 -0
- package/dist/rules/tf020.ts +123 -0
- package/dist/rules/tf021.ts +126 -0
- package/dist/rules/tf022.ts +61 -0
- package/dist/rules/tf024.ts +70 -0
- package/dist/rules/tf025.ts +101 -0
- package/dist/rules/tf026.ts +84 -0
- package/dist/rules/tf027.ts +74 -0
- package/dist/rules/tf028.ts +96 -0
- package/dist/skills/chant-terraform.md +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/package.json +7 -2
- package/src/__fixtures__/ACCEPTANCE.md +181 -0
- package/src/__fixtures__/live/main.tf +28 -0
- package/src/__fixtures__/live-adopt/estate.chdf.hcl +6 -0
- package/src/__fixtures__/live-adopt/main.tf +21 -0
- package/src/__fixtures__/live-estate/README.md +58 -0
- package/src/__fixtures__/live-estate/compute.tf +10 -0
- package/src/__fixtures__/live-estate/estate.chdf.hcl +1 -0
- package/src/__fixtures__/live-estate/logs.tf +8 -0
- package/src/__fixtures__/live-estate/network.tf +24 -0
- package/src/__fixtures__/live-estate/rule.tf +11 -0
- package/src/__fixtures__/live-estate/unowned.tf +21 -0
- package/src/__fixtures__/live-estate/versions.tf +31 -0
- package/src/__fixtures__/live-ls.json +98 -0
- package/src/__fixtures__/live-plan.json +285 -0
- package/src/__fixtures__/live-sidecar/estate.chdf.hcl +3 -0
- package/src/__fixtures__/live-sidecar/main.tf +24 -0
- package/src/__fixtures__/module-tree/README.md +16 -0
- package/src/__fixtures__/module-tree/cycle/main.tf +3 -0
- package/src/__fixtures__/module-tree/cycle/modules/a/main.tf +9 -0
- package/src/__fixtures__/module-tree/cycle/modules/b/main.tf +9 -0
- package/src/__fixtures__/module-tree/missing/main.tf +9 -0
- package/src/__fixtures__/module-tree/outside/root/main.tf +9 -0
- package/src/__fixtures__/module-tree/root/main.tf +28 -0
- package/src/__fixtures__/module-tree/root/modules/cdn/main.tf +15 -0
- package/src/__fixtures__/module-tree/root/modules/cdn/modules/bucket/main.tf +11 -0
- package/src/__fixtures__/module-tree/shared/main.tf +5 -0
- package/src/__fixtures__/references/main.tf +56 -0
- package/src/__fixtures__/remote-backend/main.tf +34 -0
- package/src/__fixtures__/with-backend/main.tf +7 -0
- package/src/codegen/lint-rules-page.test.ts +45 -0
- package/src/composites/terraform-adopt-op.acceptance.test.ts +193 -0
- package/src/composites/terraform-adopt-op.test.ts +175 -0
- package/src/composites/terraform-adopt-op.ts +202 -0
- package/src/composites/terraform-apply-op.acceptance.test.ts +188 -5
- package/src/composites/terraform-apply-op.test.ts +179 -19
- package/src/composites/terraform-apply-op.ts +123 -30
- package/src/composites/terraform-watch-op.test.ts +169 -29
- package/src/composites/terraform-watch-op.ts +183 -75
- package/src/config.ts +32 -3
- package/src/describe-resources.live.test.ts +598 -0
- package/src/describe-resources.test.ts +53 -0
- package/src/describe-resources.ts +811 -28
- package/src/hcl/descend.test.ts +162 -0
- package/src/hcl/descend.ts +273 -0
- package/src/hcl/marker.test.ts +147 -0
- package/src/hcl/marker.ts +235 -0
- package/src/hcl/parse.test.ts +69 -0
- package/src/hcl/parse.ts +269 -27
- package/src/hcl/references.test.ts +116 -0
- package/src/hcl/references.ts +172 -0
- package/src/hcl/roots.ts +59 -5
- package/src/hcl/suppressions.test.ts +147 -0
- package/src/hcl/suppressions.ts +204 -0
- package/src/hcl/value.test.ts +73 -0
- package/src/hcl/value.ts +106 -0
- package/src/index.ts +31 -1
- package/src/lint/audit-catalog.ts +288 -3
- package/src/lint/audit-lineage.ts +384 -1
- package/src/lint/audit.test.ts +210 -3
- package/src/lint/post-synth/blocks.ts +109 -0
- package/src/lint/post-synth/fixtures/TF001/negative-cloud.tf +28 -0
- package/src/lint/post-synth/fixtures/TF001/negative.tf +30 -0
- package/src/lint/post-synth/fixtures/TF001/positive-local.tf +31 -0
- package/src/lint/post-synth/fixtures/TF001/positive.tf +24 -0
- package/src/lint/post-synth/fixtures/TF002/negative.tf +17 -0
- package/src/lint/post-synth/fixtures/TF002/positive-legacy.tf +10 -0
- package/src/lint/post-synth/fixtures/TF002/positive-no-version.tf +12 -0
- package/src/lint/post-synth/fixtures/TF002/positive.tf +7 -0
- package/src/lint/post-synth/fixtures/TF003/negative.tf +10 -0
- package/src/lint/post-synth/fixtures/TF003/positive.tf +8 -0
- package/src/lint/post-synth/fixtures/TF004/negative-git.tf +3 -0
- package/src/lint/post-synth/fixtures/TF004/negative-local.tf +3 -0
- package/src/lint/post-synth/fixtures/TF004/negative.tf +4 -0
- package/src/lint/post-synth/fixtures/TF004/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/negative-local.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/negative-registry.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/negative-sha.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/negative.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/positive-branch.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/positive-non-semver.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF006/negative.tf +5 -0
- package/src/lint/post-synth/fixtures/TF006/positive-empty-default.tf +11 -0
- package/src/lint/post-synth/fixtures/TF006/positive.tf +6 -0
- package/src/lint/post-synth/fixtures/TF007/negative.tf +10 -0
- package/src/lint/post-synth/fixtures/TF007/positive.tf +9 -0
- package/src/lint/post-synth/fixtures/TF008/negative-variable.tf +10 -0
- package/src/lint/post-synth/fixtures/TF008/negative.tf +8 -0
- package/src/lint/post-synth/fixtures/TF008/positive-other-provider.tf +10 -0
- package/src/lint/post-synth/fixtures/TF008/positive.tf +5 -0
- package/src/lint/post-synth/fixtures/TF009/negative.tf +10 -0
- package/src/lint/post-synth/fixtures/TF009/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF010/negative.tf +5 -0
- package/src/lint/post-synth/fixtures/TF010/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF011/negative.tf +5 -0
- package/src/lint/post-synth/fixtures/TF011/positive-blank.tf +5 -0
- package/src/lint/post-synth/fixtures/TF011/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF012/negative.tf +4 -0
- package/src/lint/post-synth/fixtures/TF012/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF013/negative.tf +8 -0
- package/src/lint/post-synth/fixtures/TF013/positive-data.tf +8 -0
- package/src/lint/post-synth/fixtures/TF013/positive.tf +8 -0
- package/src/lint/post-synth/fixtures/TF014/negative/main.tf +26 -0
- package/src/lint/post-synth/fixtures/TF014/negative/modules/cdn/main.tf +19 -0
- package/src/lint/post-synth/fixtures/TF014/positive/main.tf +16 -0
- package/src/lint/post-synth/fixtures/TF014/positive/modules/cdn/main.tf +15 -0
- package/src/lint/post-synth/fixtures/TF015/negative/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF015/negative/modules/cdn/main.tf +16 -0
- package/src/lint/post-synth/fixtures/TF015/positive/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF015/positive/modules/cdn/main.tf +14 -0
- package/src/lint/post-synth/fixtures/TF016/negative-template.tf +7 -0
- package/src/lint/post-synth/fixtures/TF016/negative.tf +16 -0
- package/src/lint/post-synth/fixtures/TF016/positive-shared-file.tf +12 -0
- package/src/lint/post-synth/fixtures/TF016/positive.tf +13 -0
- package/src/lint/post-synth/fixtures/TF017/negative.tf +4 -0
- package/src/lint/post-synth/fixtures/TF017/positive.tf +5 -0
- package/src/lint/post-synth/fixtures/TF018/negative.tf +9 -0
- package/src/lint/post-synth/fixtures/TF018/positive-data.tf +9 -0
- package/src/lint/post-synth/fixtures/TF018/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF019/negative.tf +14 -0
- package/src/lint/post-synth/fixtures/TF019/positive-output.tf +5 -0
- package/src/lint/post-synth/fixtures/TF019/positive.tf +16 -0
- package/src/lint/post-synth/fixtures/TF020/negative.tf +43 -0
- package/src/lint/post-synth/fixtures/TF020/positive.tf +32 -0
- package/src/lint/post-synth/fixtures/TF020/scoped/main.tf +21 -0
- package/src/lint/post-synth/fixtures/TF020/scoped/modules/cdn/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF021/negative.tf +31 -0
- package/src/lint/post-synth/fixtures/TF021/positive-length.tf +8 -0
- package/src/lint/post-synth/fixtures/TF021/positive.tf +8 -0
- package/src/lint/post-synth/fixtures/TF022/negative-locator.tf +7 -0
- package/src/lint/post-synth/fixtures/TF022/negative.tf +10 -0
- package/src/lint/post-synth/fixtures/TF022/positive-nested.tf +10 -0
- package/src/lint/post-synth/fixtures/TF022/positive.tf +6 -0
- package/src/lint/post-synth/fixtures/TF023/negative/main.tf +15 -0
- package/src/lint/post-synth/fixtures/TF023/positive/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF023/positive/state.json +24 -0
- package/src/lint/post-synth/fixtures/TF024/live-clean/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF024/live-with-backend/main.tf +14 -0
- package/src/lint/post-synth/fixtures/TF024/live-with-cloud/main.tf +16 -0
- package/src/lint/post-synth/fixtures/TF025/live-clean/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF025/live-with-workspace-ref/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF026/live-default/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF026/live-explicit-delete/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF026/live-keep/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF027/live-default/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF027/live-untagged-delete/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF027/live-untagged-delete-scoped/main.tf +17 -0
- package/src/lint/post-synth/fixtures/TF027/live-untagged-report/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF028/live/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF028/stock/main.tf +10 -0
- package/src/lint/post-synth/fixtures/load.ts +108 -0
- package/src/lint/post-synth/index.ts +52 -0
- package/src/lint/post-synth/live-refusals-config-format.test.ts +143 -0
- package/src/lint/post-synth/module-rules.test.ts +153 -0
- package/src/lint/post-synth/module-source.test.ts +75 -0
- package/src/lint/post-synth/module-source.ts +121 -0
- package/src/lint/post-synth/post-synth.test.ts +73 -16
- package/src/lint/post-synth/scope.ts +107 -0
- package/src/lint/post-synth/single-block.test.ts +263 -0
- package/src/lint/post-synth/suppression.test.ts +110 -0
- package/src/lint/post-synth/tf001.ts +98 -18
- package/src/lint/post-synth/tf002.test.ts +93 -0
- package/src/lint/post-synth/tf002.ts +151 -0
- package/src/lint/post-synth/tf003.test.ts +75 -0
- package/src/lint/post-synth/tf003.ts +71 -0
- package/src/lint/post-synth/tf004.test.ts +47 -0
- package/src/lint/post-synth/tf004.ts +63 -0
- package/src/lint/post-synth/tf005.test.ts +57 -0
- package/src/lint/post-synth/tf005.ts +76 -0
- package/src/lint/post-synth/tf006.ts +53 -0
- package/src/lint/post-synth/tf007.ts +78 -0
- package/src/lint/post-synth/tf008.ts +75 -0
- package/src/lint/post-synth/tf009.ts +59 -0
- package/src/lint/post-synth/tf010.ts +51 -0
- package/src/lint/post-synth/tf011.ts +49 -0
- package/src/lint/post-synth/tf012.ts +47 -0
- package/src/lint/post-synth/tf013.ts +63 -0
- package/src/lint/post-synth/tf014.ts +72 -0
- package/src/lint/post-synth/tf015.ts +72 -0
- package/src/lint/post-synth/tf016.ts +75 -0
- package/src/lint/post-synth/tf017.ts +50 -0
- package/src/lint/post-synth/tf018.ts +72 -0
- package/src/lint/post-synth/tf019.ts +72 -0
- package/src/lint/post-synth/tf020.ts +123 -0
- package/src/lint/post-synth/tf021.ts +126 -0
- package/src/lint/post-synth/tf022.ts +61 -0
- package/src/lint/post-synth/tf024.test.ts +42 -0
- package/src/lint/post-synth/tf024.ts +70 -0
- package/src/lint/post-synth/tf025.test.ts +46 -0
- package/src/lint/post-synth/tf025.ts +101 -0
- package/src/lint/post-synth/tf026.test.ts +56 -0
- package/src/lint/post-synth/tf026.ts +84 -0
- package/src/lint/post-synth/tf027.test.ts +62 -0
- package/src/lint/post-synth/tf027.ts +74 -0
- package/src/lint/post-synth/tf028.test.ts +92 -0
- package/src/lint/post-synth/tf028.ts +96 -0
- package/src/lint/presets.test.ts +72 -0
- package/src/lint/rules/fixtures/TF101/negative-op.ts +2 -0
- package/src/lint/rules/fixtures/TF101/positive-op.ts +2 -0
- package/src/lint/rules/fixtures/load.ts +53 -0
- package/src/lint/rules/plan-before-apply.test.ts +112 -7
- package/src/lint/rules/plan-before-apply.ts +11 -0
- package/src/lint/secret-shape.test.ts +150 -0
- package/src/lint/secret-shape.ts +159 -0
- package/src/live-ownership.ts +60 -0
- package/src/lsp/completions.test.ts +3 -2
- package/src/lsp/option-keys.ts +12 -6
- package/src/op/activities/choudoufu.acceptance.test.ts +127 -0
- package/src/op/activities/choudoufu.test.ts +725 -0
- package/src/op/activities/index.ts +40 -4
- package/src/op/activities/live-detect.test.ts +143 -0
- package/src/op/activities/live-detect.ts +162 -0
- package/src/op/activities/registry.test.ts +11 -3
- package/src/op/activities/terraform.test.ts +7 -3
- package/src/op/activities/terraform.ts +861 -69
- package/src/op/activity-contracts.test.ts +147 -0
- package/src/op/activity-contracts.ts +178 -0
- package/src/op/adoption.test.ts +170 -0
- package/src/op/adoption.ts +241 -0
- package/src/op/builders.test.ts +60 -5
- package/src/op/builders.ts +95 -5
- package/src/op/resolve-root-mode.test.ts +85 -0
- package/src/op/resolve-root-mode.ts +86 -0
- package/src/plugin.test.ts +106 -0
- package/src/plugin.ts +186 -29
- package/src/skills/chant-terraform.md +1 -1
- package/src/validate.ts +10 -1
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Shaped after `lexicons/k3s/src/op/activities/k3s.ts`: `promisify(exec)` with
|
|
6
6
|
* the caller's `AbortSignal` forwarded so a local timeout or Ctrl-C kills the
|
|
7
|
-
* child,
|
|
8
|
-
*
|
|
9
|
-
* test can assert on it without running terraform.
|
|
7
|
+
* child, and every command and environment string produced by a pure exported
|
|
8
|
+
* function so a test can assert on it without running terraform.
|
|
10
9
|
*
|
|
11
10
|
* Two invariants hold for every call:
|
|
12
11
|
*
|
|
@@ -18,17 +17,41 @@
|
|
|
18
17
|
* (terraform answers `flag provided but not defined: -input`), so it
|
|
19
18
|
* carries the environment variable alone.
|
|
20
19
|
*
|
|
21
|
-
* `terraformApply` takes a saved plan file and nothing else
|
|
22
|
-
* re-plans at apply time, which is exactly the gap an
|
|
23
|
-
* close, so it is refused rather than offered.
|
|
20
|
+
* `terraformApply` takes a saved plan file and nothing else on a stock root,
|
|
21
|
+
* since a bare apply re-plans at apply time, which is exactly the gap an
|
|
22
|
+
* approval gate exists to close, so it is refused rather than offered. On a
|
|
23
|
+
* live root (#2103, `terraform.binary: "choudoufu"` plus a declared estate)
|
|
24
|
+
* it is the opposite: choudoufu refuses a plan file by design, so
|
|
25
|
+
* `terraformApply` refuses one there and runs a bare `apply -auto-approve`
|
|
26
|
+
* instead.
|
|
27
|
+
*
|
|
28
|
+
* `choudoufuLivePlan`, `choudoufuLiveLs` and `choudoufuLiveCheck` (#2103) are
|
|
29
|
+
* choudoufu-only activities living in this same module for the same reason
|
|
30
|
+
* the four above do: pure command builders, the `promisify(exec)` shape, and
|
|
31
|
+
* no dependency on the lexicon's HCL parse. `./live-detect.ts` is a
|
|
32
|
+
* deliberately separate, cheap regex-based read of "does this root declare a
|
|
33
|
+
* live estate," used only to decide how `terraformApply` reads a non-zero
|
|
34
|
+
* exit and to
|
|
35
|
+
* auto-detect `-estate` for the two live-only reads that need it; the
|
|
36
|
+
* accurate, AST-based version lives in `../../hcl/parse.ts` and stays out of
|
|
37
|
+
* this module's dependency graph. `choudoufu version` is checked once per
|
|
38
|
+
* module load (`ensureChoudoufuVersion`) and refuses a binary older than
|
|
39
|
+
* {@link MIN_CHOUDOUFU_VERSION}.
|
|
24
40
|
*/
|
|
25
41
|
|
|
26
42
|
import { exec } from "node:child_process";
|
|
43
|
+
import { writeFileSync } from "node:fs";
|
|
27
44
|
import { promisify } from "node:util";
|
|
28
|
-
import { resolve, dirname } from "node:path";
|
|
29
|
-
import { safeHeartbeat } from "@intentius/chant/op";
|
|
45
|
+
import { resolve, dirname, join } from "node:path";
|
|
30
46
|
import { loadChantConfigUpward } from "@intentius/chant/config";
|
|
31
47
|
import type { TerraformConfig, TerraformRootConfig } from "../../config";
|
|
48
|
+
import { detectLiveEstate } from "./live-detect";
|
|
49
|
+
import {
|
|
50
|
+
parseAdoptionCommands,
|
|
51
|
+
readAdoptionLedger,
|
|
52
|
+
renderAdoptionLedger,
|
|
53
|
+
type AdoptionCandidate,
|
|
54
|
+
} from "../adoption";
|
|
32
55
|
|
|
33
56
|
const execAsync = promisify(exec);
|
|
34
57
|
|
|
@@ -39,15 +62,52 @@ const execAsync = promisify(exec);
|
|
|
39
62
|
*/
|
|
40
63
|
const MAX_BUFFER = 64 * 1024 * 1024;
|
|
41
64
|
|
|
42
|
-
/** Heartbeat cadence for the long calls, matching k3s's installer loop. */
|
|
43
|
-
const HEARTBEAT_MS = 15_000;
|
|
44
|
-
|
|
45
65
|
/** Plan file written into the root directory when a step names none. */
|
|
46
66
|
export const DEFAULT_PLAN_FILE = "chant.tfplan";
|
|
47
67
|
|
|
48
68
|
/** Binary used when the `terraform` namespace records no preference. */
|
|
49
69
|
export const DEFAULT_TERRAFORM_BINARY = "terraform";
|
|
50
70
|
|
|
71
|
+
/** Where {@link choudoufuLivePlan} writes GitHub issue #788's JSON document, relative to the root directory. */
|
|
72
|
+
export const DEFAULT_LIVE_PLAN_DOCUMENT_FILE = "chant.live-plan.json";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* choudoufu below this refuses. v0.12.0 brought `live-plan -json`, `live-ls`
|
|
76
|
+
* and `live-check -json` (#2103); v0.13.0 brought the approval artifact
|
|
77
|
+
* ([choudoufu #878](https://github.com/INTENTIUS/choudoufu/issues/878), PR
|
|
78
|
+
* 889): under a live block `plan -out=FILE` is accepted and `apply FILE`
|
|
79
|
+
* re-plans the live system and refuses on a mismatch with exit status 3.
|
|
80
|
+
* v0.14.0 brought the `-json` document on a configuration that names its own
|
|
81
|
+
* estate ([choudoufu #894](https://github.com/INTENTIUS/choudoufu/issues/894),
|
|
82
|
+
* PR 915), which is the shape every chant live root has: before it,
|
|
83
|
+
* {@link choudoufuLivePlan} could not produce the document for a real root at
|
|
84
|
+
* all. {@link terraformApply} depends on the exit-3 refusal and
|
|
85
|
+
* `describeResources()` and both document-reading Ops depend on the document,
|
|
86
|
+
* so the floor is the release that shipped the later of the two.
|
|
87
|
+
*/
|
|
88
|
+
export const MIN_CHOUDOUFU_VERSION = "0.14.0";
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* choudoufu's own refusal summary for `-out` on the `live-plan -estate`
|
|
92
|
+
* surface (`internal/command/live_mode.go`). That is the one surface where a
|
|
93
|
+
* saved plan file is still refused: a directory reached through `-estate` has
|
|
94
|
+
* no `live` block, so plain `plan` and `apply` there are ordinary state-backed
|
|
95
|
+
* commands and a file written from this form would be applied by something
|
|
96
|
+
* that never heard of the estate. Under a `live` block `plan -out` is
|
|
97
|
+
* accepted (choudoufu #878).
|
|
98
|
+
*/
|
|
99
|
+
export const CHOUDOUFU_LIVE_PLAN_OUT_REFUSAL =
|
|
100
|
+
"Saved plan files are not available under live resource markers";
|
|
101
|
+
|
|
102
|
+
/** Exit status choudoufu's approval refusals carry: not 1 (any ordinary failure), not 2 (`-detailed-exitcode`). */
|
|
103
|
+
export const CHOUDOUFU_APPROVAL_EXIT_CODE = 3;
|
|
104
|
+
|
|
105
|
+
/** Summary line of choudoufu's refusal when the fresh live plan differs from the approved plan file. */
|
|
106
|
+
export const CHOUDOUFU_APPROVAL_MISMATCH_REFUSAL = "The approved plan no longer matches the live system";
|
|
107
|
+
|
|
108
|
+
/** Summary line of choudoufu's sibling refusal when the plan file was produced against another estate. */
|
|
109
|
+
export const CHOUDOUFU_WRONG_ESTATE_REFUSAL = "The approved plan belongs to a different estate";
|
|
110
|
+
|
|
51
111
|
// ── Args and results ────────────────────────────────────────────────────────
|
|
52
112
|
|
|
53
113
|
/** Fields every activity here takes: which root, and where the project is. */
|
|
@@ -81,11 +141,22 @@ export interface TerraformPlanArgs extends TerraformRootArgs {
|
|
|
81
141
|
|
|
82
142
|
export interface TerraformApplyArgs extends TerraformRootArgs {
|
|
83
143
|
/**
|
|
84
|
-
* The saved plan file to apply, relative to the root directory
|
|
144
|
+
* The saved plan file to apply, relative to the root directory, normally
|
|
85
145
|
* `plan.out.planFile`, the Plan step's own output. Required: this activity
|
|
86
|
-
* has no bare-apply mode.
|
|
146
|
+
* applies a saved plan and has no bare-apply mode, on either kind of root.
|
|
147
|
+
*
|
|
148
|
+
* On a live root this file is the approval artifact
|
|
149
|
+
* [choudoufu #878](https://github.com/INTENTIUS/choudoufu/issues/878) asked
|
|
150
|
+
* for, in the stock form choudoufu v0.13.0 shipped (PR 889). `plan -out`
|
|
151
|
+
* writes stock's own plan file; `apply <planfile>` never replays it, and
|
|
152
|
+
* never reads it as prior state or as an ownership answer. It re-plans
|
|
153
|
+
* against the live system the way every live-markers run does, then
|
|
154
|
+
* compares its fresh plan with the file's, down to the planned values, and
|
|
155
|
+
* refuses with exit status 3 when they differ. So the seam #2106 left named
|
|
156
|
+
* and unused on these args is now this field itself, and there is nothing
|
|
157
|
+
* separate to pass.
|
|
87
158
|
*/
|
|
88
|
-
planFile
|
|
159
|
+
planFile?: string;
|
|
89
160
|
}
|
|
90
161
|
|
|
91
162
|
export interface TerraformShowArgs extends TerraformRootArgs {
|
|
@@ -93,6 +164,56 @@ export interface TerraformShowArgs extends TerraformRootArgs {
|
|
|
93
164
|
planFile?: string;
|
|
94
165
|
}
|
|
95
166
|
|
|
167
|
+
export interface ChoudoufuLivePlanArgs extends TerraformRootArgs {
|
|
168
|
+
/**
|
|
169
|
+
* The estate whose ownership markers this run looks for (`-estate`).
|
|
170
|
+
* Omitted, it is auto-detected from the root's `live` block or
|
|
171
|
+
* `estate.chdf.hcl` sidecar, the same estate the root's own configuration
|
|
172
|
+
* declares, which is what a live root's every other activity assumes too.
|
|
173
|
+
*/
|
|
174
|
+
estate?: string;
|
|
175
|
+
/**
|
|
176
|
+
* Render the human half as choudoufu's adoption ledger (`-adoption-only`)
|
|
177
|
+
* instead of the ordinary plan (#2105).
|
|
178
|
+
*
|
|
179
|
+
* This activity already makes two live reads, one for the `-json` document
|
|
180
|
+
* and one for the human render, because the document carries no rendering of
|
|
181
|
+
* the plan itself. This flag changes only the second of them, which is what
|
|
182
|
+
* makes it possible at all: choudoufu refuses `-adoption-only` and `-json` on
|
|
183
|
+
* one invocation ("this run cannot produce both reports at once"), so the two
|
|
184
|
+
* reports have to come from two runs whether or not chant wants both. The
|
|
185
|
+
* cost is that the second read asks the estate-wide sweep the account-bounded
|
|
186
|
+
* question ("which live resources carry no ownership marker at all"), so it
|
|
187
|
+
* is slower than the ordinary render, not faster.
|
|
188
|
+
*/
|
|
189
|
+
adoptionOnly?: boolean;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface ChoudoufuLiveLsArgs extends TerraformRootArgs {
|
|
193
|
+
/** The estate to list (`-estate`). Omitted, auto-detected the same way {@link ChoudoufuLivePlanArgs.estate} is. */
|
|
194
|
+
estate?: string;
|
|
195
|
+
/** `-consistent`: poll past the Resource Groups Tagging API's eventual-consistency window. */
|
|
196
|
+
consistent?: boolean;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export type ChoudoufuLiveCheckArgs = TerraformRootArgs;
|
|
200
|
+
|
|
201
|
+
export interface ChoudoufuAdoptArgs extends TerraformRootArgs {
|
|
202
|
+
/**
|
|
203
|
+
* The adoptable matches to claim, normally `ledger.out.adoptions` from a
|
|
204
|
+
* {@link choudoufuLivePlan} step run with `adoptionOnly`. Each carries the
|
|
205
|
+
* two marker values and, where choudoufu printed one, the tagging command
|
|
206
|
+
* that writes them.
|
|
207
|
+
*/
|
|
208
|
+
adoptions: AdoptionCandidate[];
|
|
209
|
+
/**
|
|
210
|
+
* The contested candidates the same ledger refused, normally
|
|
211
|
+
* `ledger.out.contested`. Passed in so the Op's result names what was not
|
|
212
|
+
* adopted and why, beside what was; nothing here is ever written.
|
|
213
|
+
*/
|
|
214
|
+
contested?: AdoptionCandidate[];
|
|
215
|
+
}
|
|
216
|
+
|
|
96
217
|
/** What {@link terraformInit} resolved. */
|
|
97
218
|
export interface TerraformInitResult {
|
|
98
219
|
/** Absolute path of the root module directory that was initialized. */
|
|
@@ -125,14 +246,155 @@ export interface TerraformPlanResult extends PlanChangeCounts {
|
|
|
125
246
|
text: string;
|
|
126
247
|
}
|
|
127
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Why a live apply refused the approval artifact it was handed. Both are
|
|
251
|
+
* choudoufu's exit status 3, which is neither 1 (any ordinary failure, which
|
|
252
|
+
* a pipeline must not route back to a reviewer) nor `-detailed-exitcode`'s 2.
|
|
253
|
+
*
|
|
254
|
+
* - `"approval-mismatch"`: the fresh live plan's change set or planned
|
|
255
|
+
* values differ from the approved file's.
|
|
256
|
+
* - `"wrong-estate"`: the file was produced from a configuration naming
|
|
257
|
+
* another estate.
|
|
258
|
+
*/
|
|
259
|
+
export type TerraformApprovalRefusal = "approval-mismatch" | "wrong-estate";
|
|
260
|
+
|
|
128
261
|
/** What {@link terraformApply} resolved. */
|
|
129
262
|
export interface TerraformApplyResult {
|
|
130
|
-
/** The plan file that was applied. */
|
|
131
|
-
planFile
|
|
263
|
+
/** The plan file that was applied, or that was refused. */
|
|
264
|
+
planFile?: string;
|
|
132
265
|
/** Absolute path of the root module directory. */
|
|
133
266
|
dir: string;
|
|
134
|
-
/**
|
|
267
|
+
/** `true` when the plan was applied. `false` exactly when {@link refused} is set. */
|
|
135
268
|
applied: boolean;
|
|
269
|
+
/**
|
|
270
|
+
* Set when a live apply refused the approval artifact (choudoufu exit 3).
|
|
271
|
+
* This is a result, not a thrown error, so a workflow can route the run
|
|
272
|
+
* back to review instead of treating it as a broken step.
|
|
273
|
+
*/
|
|
274
|
+
refused?: TerraformApprovalRefusal;
|
|
275
|
+
/** choudoufu's own refusal text, verbatim, when {@link refused} is set. */
|
|
276
|
+
refusal?: string;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Counts projected out of `live-plan -json`'s `unowned` section. */
|
|
280
|
+
export interface LivePlanUnownedCounts {
|
|
281
|
+
/** Live resources at a declared identity carrying no ownership marker for this estate. */
|
|
282
|
+
unowned: number;
|
|
283
|
+
/** Of those, how many an exact content match makes adoptable. */
|
|
284
|
+
adoptable: number;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* What {@link choudoufuLivePlan} resolved. `adds`/`changes`/`destroys` (#2106,
|
|
289
|
+
* {@link PlanChangeCounts}) are parsed off the human-readable render's own
|
|
290
|
+
* "Plan: N to add, N to change, N to destroy." summary line
|
|
291
|
+
* ({@link parseChoudoufuPlanSummary}), not off GitHub issue #788's JSON
|
|
292
|
+
* document: that document is deliberately scoped to the three sections a
|
|
293
|
+
* stock plan's `-json` has no notion of at all (`bound`, `omissions`,
|
|
294
|
+
* `unowned`; `internal/command/views/live_plan.go`'s own `LivePlanDocument`
|
|
295
|
+
* doc comment says as much — "the stock plan's -json...can stay separate")
|
|
296
|
+
* and carries no resource-change diff of its own. The ordinary create/change/
|
|
297
|
+
* destroy diff — including the delete choudoufu's own `undeclared_tagged`
|
|
298
|
+
* quadrant folds into it for an orphaned marked resource, per that same
|
|
299
|
+
* file's `StatelessRemoval` doc comment ("the plan's own resource diff
|
|
300
|
+
* carries the destroy itself for those") — renders only in the human form,
|
|
301
|
+
* the same summary line a stock `terraform plan` prints
|
|
302
|
+
* (`internal/command/views/operation_test.go`'s own fixtures confirm the
|
|
303
|
+
* exact wording), which is why the human run's text, already fetched here for
|
|
304
|
+
* the approver to read, is what this activity parses instead of the JSON.
|
|
305
|
+
*/
|
|
306
|
+
export interface ChoudoufuLivePlanResult extends LivePlanUnownedCounts, PlanChangeCounts {
|
|
307
|
+
/** `true` when `-detailed-exitcode` reported exit 2: the live plan proposes changes. */
|
|
308
|
+
drift: boolean;
|
|
309
|
+
/** GitHub issue #788's JSON document (`bound`, `omissions`, `unowned`), captured whole. */
|
|
310
|
+
json: unknown;
|
|
311
|
+
/**
|
|
312
|
+
* The human-readable plan, from a second `live-plan` run without `-json`
|
|
313
|
+
* (the document carries no render of it) — or, under
|
|
314
|
+
* {@link ChoudoufuLivePlanArgs.adoptionOnly}, choudoufu's own adoption ledger.
|
|
315
|
+
*/
|
|
316
|
+
text: string;
|
|
317
|
+
/**
|
|
318
|
+
* chant's rendering of the adoption ledger over the `-json` document
|
|
319
|
+
* (#2105): one line per adoptable match with its address, its live identity
|
|
320
|
+
* and the two marker values, then any contested address, listed and never
|
|
321
|
+
* offered. Present on every run, `-adoption-only` or not, since it is a
|
|
322
|
+
* projection of the document rather than a second report.
|
|
323
|
+
*/
|
|
324
|
+
ledger: string;
|
|
325
|
+
/**
|
|
326
|
+
* What a finding mode posts: {@link ChoudoufuLivePlanResult.text} and then
|
|
327
|
+
* {@link ChoudoufuLivePlanResult.ledger}. The one field an issue or PR body
|
|
328
|
+
* references, so that the JSON document has no path into a body at all.
|
|
329
|
+
*/
|
|
330
|
+
finding: string;
|
|
331
|
+
/** Adoptable matches: exactly one live resource at the declared address, with the marker pair that claims it. */
|
|
332
|
+
adoptions: AdoptionCandidate[];
|
|
333
|
+
/** Every candidate at a declared address more than one live resource sits at. Never adoptable. */
|
|
334
|
+
contested: AdoptionCandidate[];
|
|
335
|
+
/** How many declared addresses are contested — the count `contested` above spells out. */
|
|
336
|
+
ambiguous: number;
|
|
337
|
+
/** Absolute path of the root module directory. */
|
|
338
|
+
dir: string;
|
|
339
|
+
/** Where the JSON document was written, relative to `dir`; {@link DEFAULT_LIVE_PLAN_DOCUMENT_FILE} unless overridden. */
|
|
340
|
+
documentPath: string;
|
|
341
|
+
/** The estate this plan ran against. */
|
|
342
|
+
estate: string;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** What {@link choudoufuLiveLs} resolved. */
|
|
346
|
+
export interface ChoudoufuLiveLsResult {
|
|
347
|
+
/** The `-json` listing, parsed: every resource the account holds under the estate. */
|
|
348
|
+
json: unknown;
|
|
349
|
+
/** Absolute path of the root module directory (`live-ls` needs no configuration, but this activity still runs in it). */
|
|
350
|
+
dir: string;
|
|
351
|
+
/** The estate that was listed. */
|
|
352
|
+
estate: string;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** One adoption {@link choudoufuAdopt} declined, with the reason an operator acts on. */
|
|
356
|
+
export interface AdoptionRefusal {
|
|
357
|
+
/** The declared instance address that was not claimed. */
|
|
358
|
+
addr: string;
|
|
359
|
+
/** The live resource's identity, when the refusal is about a particular one. */
|
|
360
|
+
identity: string;
|
|
361
|
+
/** Why, in a sentence, always naming the two marker values a hand-write would need. */
|
|
362
|
+
reason: string;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** What {@link choudoufuAdopt} resolved. */
|
|
366
|
+
export interface ChoudoufuAdoptResult {
|
|
367
|
+
/**
|
|
368
|
+
* How the markers were written. Only `"tag-write"` today; the field exists
|
|
369
|
+
* because #2105 weighed a second mechanism (choudoufu's `adopt` policy verb)
|
|
370
|
+
* and a later run of this activity should say which one it used rather than
|
|
371
|
+
* leave a reader to infer it from the absence of a field. See the activity's
|
|
372
|
+
* own doc for why the policy verb is not reachable from outside the
|
|
373
|
+
* configuration.
|
|
374
|
+
*/
|
|
375
|
+
mechanism: "tag-write";
|
|
376
|
+
/** The declared addresses whose live resources now carry this estate's markers. */
|
|
377
|
+
adopted: string[];
|
|
378
|
+
/** `adopted.length`, as a scalar an outcome attribute can carry. */
|
|
379
|
+
adoptedCount: number;
|
|
380
|
+
/** Every candidate this run declined, contested addresses included, with the reason. */
|
|
381
|
+
refused: AdoptionRefusal[];
|
|
382
|
+
/** How many declared addresses were contested — never adopted, always reported. */
|
|
383
|
+
ambiguous: number;
|
|
384
|
+
/** Absolute path of the root module directory. */
|
|
385
|
+
dir: string;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** What {@link choudoufuLiveCheck} resolved. */
|
|
389
|
+
export interface ChoudoufuLiveCheckResult {
|
|
390
|
+
/** `true` when the configuration is refused under live resource markers (a non-zero exit). */
|
|
391
|
+
refused: boolean;
|
|
392
|
+
/** GitHub issue #790's declared roster, parsed, when the output could be parsed as JSON. */
|
|
393
|
+
json: unknown;
|
|
394
|
+
/** The raw `-json` output. */
|
|
395
|
+
text: string;
|
|
396
|
+
/** Absolute path of the root module directory. */
|
|
397
|
+
dir: string;
|
|
136
398
|
}
|
|
137
399
|
|
|
138
400
|
/** What {@link terraformShow} resolved. */
|
|
@@ -212,11 +474,91 @@ export function terraformPlanCommand(opts: {
|
|
|
212
474
|
return parts.join(" ");
|
|
213
475
|
}
|
|
214
476
|
|
|
215
|
-
/**
|
|
477
|
+
/**
|
|
478
|
+
* `terraform apply <planFile>` — a saved plan, never a bare apply. The same
|
|
479
|
+
* command on a live root: choudoufu v0.13.0 admits a plan file under a `live`
|
|
480
|
+
* block, and a matched one applies without re-prompting, exactly as stock's
|
|
481
|
+
* own `apply <planfile>` does, so no `-auto-approve` is needed or passed.
|
|
482
|
+
*/
|
|
216
483
|
export function terraformApplyCommand(opts: { binary: string; planFile: string }): string {
|
|
217
484
|
return `${opts.binary} apply -input=false ${quoteArg(opts.planFile)}`;
|
|
218
485
|
}
|
|
219
486
|
|
|
487
|
+
/**
|
|
488
|
+
* `live-plan -detailed-exitcode [-json] [-estate=<estate>]`. `-json` is what
|
|
489
|
+
* prints GitHub issue #788's document instead of the plan; the human render
|
|
490
|
+
* needs a second invocation without it (see {@link choudoufuLivePlan}).
|
|
491
|
+
*
|
|
492
|
+
* `estate` is omitted exactly when the configuration names its own estate.
|
|
493
|
+
* choudoufu refuses `-estate` beside a `live` block or an `estate.chdf.hcl`
|
|
494
|
+
* sidecar ("Estate named by both the live block and -estate"), and since
|
|
495
|
+
* v0.14.0 it does not need one: the run reads the declared name itself and
|
|
496
|
+
* prints the same document ([choudoufu #894](https://github.com/INTENTIUS/choudoufu/issues/894),
|
|
497
|
+
* PR 915). The flag is still how a root that declares nothing names an
|
|
498
|
+
* estate for the run.
|
|
499
|
+
*
|
|
500
|
+
* `adoptionOnly` makes that second invocation print the adoption ledger and
|
|
501
|
+
* nothing else (GitHub issue #587). It is refused here alongside `json`
|
|
502
|
+
* because choudoufu refuses the pair itself, and a builder that emitted a
|
|
503
|
+
* command the binary rejects would move the error to the far side of a
|
|
504
|
+
* process spawn for no gain.
|
|
505
|
+
*/
|
|
506
|
+
export function choudoufuLivePlanCommand(opts: {
|
|
507
|
+
binary: string;
|
|
508
|
+
estate?: string;
|
|
509
|
+
json: boolean;
|
|
510
|
+
adoptionOnly?: boolean;
|
|
511
|
+
noColor?: boolean;
|
|
512
|
+
}): string {
|
|
513
|
+
if (opts.json && opts.adoptionOnly) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
"choudoufuLivePlanCommand: -adoption-only and -json cannot be combined. choudoufu refuses the pair " +
|
|
516
|
+
"(-adoption-only asks for the adoption ledger and -json asks for the bound/omissions/unowned " +
|
|
517
|
+
"document), so the two reports come from two runs.",
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
const parts = [opts.binary, "live-plan", "-detailed-exitcode"];
|
|
521
|
+
if (opts.json) parts.push("-json");
|
|
522
|
+
if (opts.adoptionOnly) parts.push("-adoption-only");
|
|
523
|
+
if (opts.noColor) parts.push("-no-color");
|
|
524
|
+
if (opts.estate !== undefined) parts.push(`-estate=${quoteArg(opts.estate)}`);
|
|
525
|
+
return parts.join(" ");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** `live-ls -estate=<estate> -json [-consistent]`. */
|
|
529
|
+
export function choudoufuLiveLsCommand(opts: { binary: string; estate: string; consistent?: boolean }): string {
|
|
530
|
+
const parts = [opts.binary, "live-ls", `-estate=${quoteArg(opts.estate)}`, "-json"];
|
|
531
|
+
if (opts.consistent) parts.push("-consistent");
|
|
532
|
+
return parts.join(" ");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* `live-check -json`. No `DIR` argument: the activity already runs in the
|
|
537
|
+
* root directory (`cwd`), so `-json` alone checks it, since DIR defaults to
|
|
538
|
+
* `.`. Makes no cloud calls; a non-zero exit is a refusal, not a failure this
|
|
539
|
+
* builder's caller need distinguish from one.
|
|
540
|
+
*/
|
|
541
|
+
export function choudoufuLiveCheckCommand(opts: { binary: string }): string {
|
|
542
|
+
return `${opts.binary} live-check -json`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Project `live-plan -json`'s `unowned` section into counts: how many live
|
|
547
|
+
* resources at a declared identity carry no ownership marker for this
|
|
548
|
+
* estate, and of those, how many an exact content match makes adoptable
|
|
549
|
+
* (`adopt_tofu_estate`/`adopt_tofu_address` present).
|
|
550
|
+
*/
|
|
551
|
+
export function countLivePlanUnowned(document: unknown): LivePlanUnownedCounts {
|
|
552
|
+
const unowned = (document as { unowned?: unknown } | null | undefined)?.unowned;
|
|
553
|
+
if (!Array.isArray(unowned)) return { unowned: 0, adoptable: 0 };
|
|
554
|
+
let adoptable = 0;
|
|
555
|
+
for (const entry of unowned) {
|
|
556
|
+
const e = entry as { adopt_tofu_estate?: unknown; adopt_tofu_address?: unknown } | null | undefined;
|
|
557
|
+
if (e?.adopt_tofu_estate || e?.adopt_tofu_address) adoptable++;
|
|
558
|
+
}
|
|
559
|
+
return { unowned: unowned.length, adoptable };
|
|
560
|
+
}
|
|
561
|
+
|
|
220
562
|
/**
|
|
221
563
|
* `terraform show`. The one command here that takes no `-input` flag
|
|
222
564
|
* (terraform answers `flag provided but not defined: -input`), so automation
|
|
@@ -248,6 +590,94 @@ export function countPlanChanges(planJson: unknown): PlanChangeCounts {
|
|
|
248
590
|
return counts;
|
|
249
591
|
}
|
|
250
592
|
|
|
593
|
+
/**
|
|
594
|
+
* Parse the ordinary "Plan: N to add, N to change, N to destroy." (or "...N
|
|
595
|
+
* to import, N to add...") summary line out of a human-readable `live-plan`
|
|
596
|
+
* render (#2106). "No changes." — the exit-0 case — matches nothing and
|
|
597
|
+
* reads as zero on every count, the same conservative default
|
|
598
|
+
* {@link countPlanChanges} falls back to on a shape it does not recognize.
|
|
599
|
+
*/
|
|
600
|
+
export function parseChoudoufuPlanSummary(text: string): PlanChangeCounts {
|
|
601
|
+
const match = /Plan:\s*(?:\d+\s+to import,\s*)?(\d+)\s+to add,\s*(\d+)\s+to change,\s*(\d+)\s+to destroy\./.exec(
|
|
602
|
+
text,
|
|
603
|
+
);
|
|
604
|
+
if (!match) return { adds: 0, changes: 0, destroys: 0 };
|
|
605
|
+
return { adds: Number(match[1]), changes: Number(match[2]), destroys: Number(match[3]) };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// ── choudoufu version check ─────────────────────────────────────────────────
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Parse a semver-ish string's leading `major.minor.patch`, ignoring anything
|
|
612
|
+
* after (a `-dev`/`-rc1` suffix, say). Unparseable input reads as `0.0.0`,
|
|
613
|
+
* which sorts as "older than everything" rather than crashing, since a
|
|
614
|
+
* version string this check cannot make sense of should not be quietly trusted.
|
|
615
|
+
*/
|
|
616
|
+
function parseVersionCore(version: string): [number, number, number] {
|
|
617
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
618
|
+
if (!m) return [0, 0, 0];
|
|
619
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Whether `version` is strictly older than `min`, comparing `major.minor.patch` numerically. */
|
|
623
|
+
export function isOlderVersion(version: string, min: string): boolean {
|
|
624
|
+
const a = parseVersionCore(version);
|
|
625
|
+
const b = parseVersionCore(min);
|
|
626
|
+
for (let i = 0; i < 3; i++) {
|
|
627
|
+
if (a[i] !== b[i]) return a[i] < b[i];
|
|
628
|
+
}
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Extract the release version from `choudoufu version`'s human-readable
|
|
634
|
+
* output: `"choudoufu v0.13.0 (based on OpenTofu v1.13.0)\non darwin_arm64"`.
|
|
635
|
+
* `choudoufu version -json`'s `terraform_version` field is the wrong number
|
|
636
|
+
* here: it names the upstream OpenTofu base this build forked from, not the
|
|
637
|
+
* choudoufu release itself (`internal/command/views/version.go`), so this
|
|
638
|
+
* reads the human form's `choudoufu vX.Y.Z` prefix instead. A dev build with
|
|
639
|
+
* no release tag baked in (`version.Fork` unset) prints only the upstream
|
|
640
|
+
* line and carries no choudoufu version to check at all: `undefined` here,
|
|
641
|
+
* same as an output this cannot parse.
|
|
642
|
+
*/
|
|
643
|
+
export function parseChoudoufuVersion(versionOutput: string): string | undefined {
|
|
644
|
+
const m = /^choudoufu\s+v?(\d+\.\d+\.\d+)/m.exec(versionOutput);
|
|
645
|
+
return m?.[1];
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** Cached per activity-module load (#2103): `choudoufu version` runs at most once per process. */
|
|
649
|
+
let choudoufuVersionCheck: Promise<void> | undefined;
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Refuse a choudoufu binary older than {@link MIN_CHOUDOUFU_VERSION}. Runs
|
|
653
|
+
* `choudoufu version` (never `-json`; see {@link parseChoudoufuVersion}) at
|
|
654
|
+
* most once per module load, cached across every activity call regardless of
|
|
655
|
+
* which one triggered it. A no-op for `terraform`/`tofu`, and a no-op when
|
|
656
|
+
* the version cannot be determined at all (a dev build): this refuses a
|
|
657
|
+
* known-old binary, not an unknown one.
|
|
658
|
+
*/
|
|
659
|
+
async function ensureChoudoufuVersion(binary: string, signal?: AbortSignal): Promise<void> {
|
|
660
|
+
if (binary !== "choudoufu") return;
|
|
661
|
+
choudoufuVersionCheck ??= (async () => {
|
|
662
|
+
const { stdout } = await execAsync(`${binary} version`, { env: process.env, signal });
|
|
663
|
+
const version = parseChoudoufuVersion(stdout);
|
|
664
|
+
if (version === undefined) return;
|
|
665
|
+
if (isOlderVersion(version, MIN_CHOUDOUFU_VERSION)) {
|
|
666
|
+
throw new Error(
|
|
667
|
+
`choudoufu ${version} is older than the minimum supported version v${MIN_CHOUDOUFU_VERSION} ` +
|
|
668
|
+
"(needed for live-plan -json, live-ls, live-check -json, and the approval artifact `plan -out` / " +
|
|
669
|
+
"`apply <planfile>` pair). Upgrade choudoufu.",
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
})();
|
|
673
|
+
return choudoufuVersionCheck;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** Test-only: clear the cached version check so a test can simulate a fresh module load. */
|
|
677
|
+
export function __resetChoudoufuVersionCheckForTests(): void {
|
|
678
|
+
choudoufuVersionCheck = undefined;
|
|
679
|
+
}
|
|
680
|
+
|
|
251
681
|
// ── Root resolution ─────────────────────────────────────────────────────────
|
|
252
682
|
|
|
253
683
|
/** A root entry resolved against the project config, ready to run in. */
|
|
@@ -256,6 +686,10 @@ interface ResolvedRoot {
|
|
|
256
686
|
root: TerraformRootConfig;
|
|
257
687
|
/** Absolute path of `root.dir`, resolved against the project root. */
|
|
258
688
|
dir: string;
|
|
689
|
+
/** The declared estate, when `binary` is `"choudoufu"` and one is detected. */
|
|
690
|
+
estate?: string;
|
|
691
|
+
/** `true` exactly when this root is live: `binary` is `"choudoufu"` and `estate` is set. */
|
|
692
|
+
live: boolean;
|
|
259
693
|
}
|
|
260
694
|
|
|
261
695
|
/**
|
|
@@ -263,8 +697,12 @@ interface ResolvedRoot {
|
|
|
263
697
|
* The walk is {@link loadChantConfigUpward}, so a step invoked from a
|
|
264
698
|
* subdirectory still finds the project's `chant.config.*`, and `dir` resolves
|
|
265
699
|
* against the directory that config lives in rather than against the cwd.
|
|
700
|
+
*
|
|
701
|
+
* Also runs {@link ensureChoudoufuVersion} and, on a choudoufu root,
|
|
702
|
+
* {@link detectLiveEstate}, so every activity answers "is this root live"
|
|
703
|
+
* the same way without repeating either check itself.
|
|
266
704
|
*/
|
|
267
|
-
async function resolveRoot(args: TerraformRootArgs): Promise<ResolvedRoot> {
|
|
705
|
+
async function resolveRoot(args: TerraformRootArgs, signal?: AbortSignal): Promise<ResolvedRoot> {
|
|
268
706
|
const start = resolve(args.cwd ?? process.cwd());
|
|
269
707
|
const { config, configPath } = await loadChantConfigUpward(start);
|
|
270
708
|
const projectRoot = configPath ? dirname(configPath) : start;
|
|
@@ -279,7 +717,11 @@ async function resolveRoot(args: TerraformRootArgs): Promise<ResolvedRoot> {
|
|
|
279
717
|
(known.length > 0 ? `known roots: ${known.join(", ")}` : "the namespace declares no roots"),
|
|
280
718
|
);
|
|
281
719
|
}
|
|
282
|
-
|
|
720
|
+
const binary = terraformBinary(namespace);
|
|
721
|
+
const dir = resolve(projectRoot, root.dir);
|
|
722
|
+
await ensureChoudoufuVersion(binary, signal);
|
|
723
|
+
const estate = binary === "choudoufu" ? detectLiveEstate(dir) : undefined;
|
|
724
|
+
return { binary, root, dir, ...(estate !== undefined ? { estate } : {}), live: estate !== undefined };
|
|
283
725
|
}
|
|
284
726
|
|
|
285
727
|
/** The shape `promisify(exec)` rejects with: an Error carrying the child's exit code and output. */
|
|
@@ -299,16 +741,6 @@ async function run(
|
|
|
299
741
|
return execAsync(cmd, { cwd: dir, env: { ...process.env, ...env }, signal, maxBuffer: MAX_BUFFER });
|
|
300
742
|
}
|
|
301
743
|
|
|
302
|
-
/** Run `body` with a heartbeat ticking, so a long terraform call is not read as a hung one. */
|
|
303
|
-
async function withHeartbeat<T>(details: Record<string, unknown>, body: () => Promise<T>): Promise<T> {
|
|
304
|
-
const timer = setInterval(() => safeHeartbeat(details), HEARTBEAT_MS);
|
|
305
|
-
try {
|
|
306
|
-
return await body();
|
|
307
|
-
} finally {
|
|
308
|
-
clearInterval(timer);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
744
|
function report(stdout: string, stderr: string): void {
|
|
313
745
|
if (stdout) console.log(stdout);
|
|
314
746
|
if (stderr) console.error(stderr);
|
|
@@ -325,7 +757,7 @@ export async function terraformInit(
|
|
|
325
757
|
args: TerraformInitArgs,
|
|
326
758
|
signal?: AbortSignal,
|
|
327
759
|
): Promise<TerraformInitResult> {
|
|
328
|
-
const { binary, root, dir } = await resolveRoot(args);
|
|
760
|
+
const { binary, root, dir } = await resolveRoot(args, signal);
|
|
329
761
|
const cmd = terraformInitCommand({
|
|
330
762
|
binary,
|
|
331
763
|
...(root.backendConfig ? { backendConfig: root.backendConfig } : {}),
|
|
@@ -333,9 +765,7 @@ export async function terraformInit(
|
|
|
333
765
|
...(args.reconfigure ? { reconfigure: true } : {}),
|
|
334
766
|
});
|
|
335
767
|
|
|
336
|
-
const { stdout, stderr } = await
|
|
337
|
-
run(cmd, dir, terraformEnvironment(root), signal),
|
|
338
|
-
);
|
|
768
|
+
const { stdout, stderr } = await run(cmd, dir, terraformEnvironment(root), signal);
|
|
339
769
|
report(stdout, stderr);
|
|
340
770
|
|
|
341
771
|
return { dir, ...(root.workspace ? { workspace: root.workspace } : {}) };
|
|
@@ -358,7 +788,7 @@ export async function terraformPlan(
|
|
|
358
788
|
args: TerraformPlanArgs,
|
|
359
789
|
signal?: AbortSignal,
|
|
360
790
|
): Promise<TerraformPlanResult> {
|
|
361
|
-
const { binary, root, dir } = await resolveRoot(args);
|
|
791
|
+
const { binary, root, dir } = await resolveRoot(args, signal);
|
|
362
792
|
const planFile = args.planFile ?? DEFAULT_PLAN_FILE;
|
|
363
793
|
const env = terraformEnvironment(root);
|
|
364
794
|
const cmd = terraformPlanCommand({
|
|
@@ -368,26 +798,25 @@ export async function terraformPlan(
|
|
|
368
798
|
...(args.destroy ? { destroy: true } : {}),
|
|
369
799
|
});
|
|
370
800
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
report(failure.stdout ?? "", failure.stderr ?? "");
|
|
388
|
-
return true;
|
|
801
|
+
let changed: boolean;
|
|
802
|
+
try {
|
|
803
|
+
const { stdout, stderr } = await run(cmd, dir, env, signal);
|
|
804
|
+
report(stdout, stderr);
|
|
805
|
+
changed = false;
|
|
806
|
+
} catch (err) {
|
|
807
|
+
// An abort or a spawn failure carries no numeric exit code. That is not
|
|
808
|
+
// terraform answering, so it propagates untouched.
|
|
809
|
+
const failure = err as ExecFailure;
|
|
810
|
+
if (typeof failure.code !== "number") throw err;
|
|
811
|
+
if (failure.code !== 2) {
|
|
812
|
+
const detail = (failure.stderr ?? "").trim() || (failure.stdout ?? "").trim();
|
|
813
|
+
throw new Error(
|
|
814
|
+
`${binary} plan failed in ${dir} (exit ${failure.code})${detail ? `\n${detail}` : ""}`,
|
|
815
|
+
);
|
|
389
816
|
}
|
|
390
|
-
|
|
817
|
+
report(failure.stdout ?? "", failure.stderr ?? "");
|
|
818
|
+
changed = true;
|
|
819
|
+
}
|
|
391
820
|
|
|
392
821
|
const jsonRun = await run(terraformShowCommand({ binary, json: true, planFile }), dir, env, signal);
|
|
393
822
|
const textRun = await run(terraformShowCommand({ binary, json: false, planFile }), dir, env, signal);
|
|
@@ -397,10 +826,35 @@ export async function terraformPlan(
|
|
|
397
826
|
}
|
|
398
827
|
|
|
399
828
|
/**
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
829
|
+
* Which of choudoufu's two approval refusals an exit-3 apply printed, or
|
|
830
|
+
* `undefined` when the output carries neither summary line. Reads both the
|
|
831
|
+
* stderr the diagnostic is written to and the stdout the fresh plan is
|
|
832
|
+
* rendered on, since the two travel separately.
|
|
833
|
+
*/
|
|
834
|
+
export function classifyApprovalRefusal(output: string): TerraformApprovalRefusal | undefined {
|
|
835
|
+
if (output.includes(CHOUDOUFU_WRONG_ESTATE_REFUSAL)) return "wrong-estate";
|
|
836
|
+
if (output.includes(CHOUDOUFU_APPROVAL_MISMATCH_REFUSAL)) return "approval-mismatch";
|
|
837
|
+
return undefined;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* `terraform apply <planFile>` in the named root, on a stock root and on a
|
|
842
|
+
* live one alike.
|
|
843
|
+
*
|
|
844
|
+
* Neither kind has a bare-apply mode here: without a saved `planFile`, apply
|
|
845
|
+
* re-plans at apply time and acts on something no gate ever saw, so a missing
|
|
846
|
+
* one is refused before the CLI runs. choudoufu v0.13.0 is what makes the
|
|
847
|
+
* live half of that true (choudoufu #878, PR 889): `plan -out` is accepted
|
|
848
|
+
* under a `live` block, and `apply <planfile>` re-plans against the live
|
|
849
|
+
* system, compares its fresh plan with the file's down to the planned values,
|
|
850
|
+
* and applies only when they agree.
|
|
851
|
+
*
|
|
852
|
+
* When they do not, choudoufu exits **3**, neither 1 (any ordinary failure)
|
|
853
|
+
* nor `-detailed-exitcode`'s 2, with one of two named refusals. That is an
|
|
854
|
+
* answer rather than a breakage, so it comes back as a result (`refused`,
|
|
855
|
+
* `refusal`) instead of a thrown error, and a workflow can route the run back
|
|
856
|
+
* to review. Exit 3 is read this way only on a live root; on a stock root
|
|
857
|
+
* nothing produces it and every non-zero exit stays a failure.
|
|
404
858
|
*
|
|
405
859
|
* Uses the longInfra profile.
|
|
406
860
|
*/
|
|
@@ -408,23 +862,41 @@ export async function terraformApply(
|
|
|
408
862
|
args: TerraformApplyArgs,
|
|
409
863
|
signal?: AbortSignal,
|
|
410
864
|
): Promise<TerraformApplyResult> {
|
|
411
|
-
|
|
865
|
+
const { binary, root, dir, live } = await resolveRoot(args, signal);
|
|
866
|
+
const hasPlanFile = typeof args.planFile === "string" && args.planFile.trim() !== "";
|
|
867
|
+
|
|
868
|
+
if (!hasPlanFile) {
|
|
412
869
|
throw new Error(
|
|
413
870
|
"terraformApply: planFile is required — this activity applies a saved plan and has no bare-apply mode. " +
|
|
414
871
|
"Pass the Plan step's own output (`plan.out.planFile`).",
|
|
415
872
|
);
|
|
416
873
|
}
|
|
417
874
|
|
|
418
|
-
const
|
|
419
|
-
const cmd = terraformApplyCommand({ binary, planFile
|
|
420
|
-
|
|
421
|
-
const { stdout, stderr } = await withHeartbeat(
|
|
422
|
-
{ step: "terraform apply", root: args.root, dir, planFile: args.planFile },
|
|
423
|
-
() => run(cmd, dir, terraformEnvironment(root), signal),
|
|
424
|
-
);
|
|
425
|
-
report(stdout, stderr);
|
|
875
|
+
const planFile = args.planFile!;
|
|
876
|
+
const cmd = terraformApplyCommand({ binary, planFile });
|
|
426
877
|
|
|
427
|
-
|
|
878
|
+
try {
|
|
879
|
+
const { stdout, stderr } = await run(cmd, dir, terraformEnvironment(root), signal);
|
|
880
|
+
report(stdout, stderr);
|
|
881
|
+
return { planFile, dir, applied: true };
|
|
882
|
+
} catch (err) {
|
|
883
|
+
// An abort or a spawn failure carries no numeric exit code. That is not
|
|
884
|
+
// the CLI answering, so it propagates untouched, same as terraformPlan.
|
|
885
|
+
const failure = err as ExecFailure;
|
|
886
|
+
const refused =
|
|
887
|
+
live && failure.code === CHOUDOUFU_APPROVAL_EXIT_CODE
|
|
888
|
+
? classifyApprovalRefusal(`${failure.stderr ?? ""}\n${failure.stdout ?? ""}`)
|
|
889
|
+
: undefined;
|
|
890
|
+
if (refused === undefined) throw err;
|
|
891
|
+
report(failure.stdout ?? "", failure.stderr ?? "");
|
|
892
|
+
return {
|
|
893
|
+
planFile,
|
|
894
|
+
dir,
|
|
895
|
+
applied: false,
|
|
896
|
+
refused,
|
|
897
|
+
refusal: (failure.stderr ?? "").trim() || (failure.stdout ?? "").trim(),
|
|
898
|
+
};
|
|
899
|
+
}
|
|
428
900
|
}
|
|
429
901
|
|
|
430
902
|
/**
|
|
@@ -440,7 +912,7 @@ export async function terraformShow(
|
|
|
440
912
|
args: TerraformShowArgs,
|
|
441
913
|
signal?: AbortSignal,
|
|
442
914
|
): Promise<TerraformShowResult> {
|
|
443
|
-
const { binary, root, dir } = await resolveRoot(args);
|
|
915
|
+
const { binary, root, dir } = await resolveRoot(args, signal);
|
|
444
916
|
const env = terraformEnvironment(root);
|
|
445
917
|
const planFile = args.planFile;
|
|
446
918
|
|
|
@@ -467,3 +939,323 @@ export async function terraformShow(
|
|
|
467
939
|
...(planFile ? countPlanChanges(json) : { adds: 0, changes: 0, destroys: 0 }),
|
|
468
940
|
};
|
|
469
941
|
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* The estate a choudoufu-only activity runs against: `args.estate` when
|
|
945
|
+
* given, else the one {@link resolveRoot} auto-detected from the root's
|
|
946
|
+
* `live` block or `estate.chdf.hcl` sidecar. Throws when neither names one,
|
|
947
|
+
* since `live-plan`/`live-ls` need an estate to filter on, unlike a stock command.
|
|
948
|
+
*/
|
|
949
|
+
function resolveEstate(args: { estate?: string }, resolved: ResolvedRoot, activity: string): string {
|
|
950
|
+
const estate = args.estate ?? resolved.estate;
|
|
951
|
+
if (!estate) {
|
|
952
|
+
throw new Error(
|
|
953
|
+
`${activity}: no estate to run against. Declare one in the root's \`live\` block or ` +
|
|
954
|
+
"`estate.chdf.hcl` sidecar, or pass `estate` explicitly.",
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
return estate;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* `choudoufu live-plan -detailed-exitcode -json -estate=<estate>`, plus a
|
|
962
|
+
* second `live-plan` without `-json` for the human-readable render. GitHub
|
|
963
|
+
* issue #788's JSON document carries no render of the plan itself
|
|
964
|
+
* (`views.LivePlanDocument` has no such field), so unlike `terraformPlan`'s
|
|
965
|
+
* single saved artifact this genuinely needs two live reads.
|
|
966
|
+
*
|
|
967
|
+
* The exit code is the answer, exactly as `terraformPlan`'s is: 0 is no
|
|
968
|
+
* drift, 2 is drift, and anything else is a failure thrown with choudoufu's
|
|
969
|
+
* own stderr attached. The JSON document is written to `documentPath` (under
|
|
970
|
+
* `dir`) for a later step or reviewer to open directly.
|
|
971
|
+
*
|
|
972
|
+
* Uses the longInfra profile: like `terraformPlan`, this reads the live
|
|
973
|
+
* system in full (the estate-wide sweep).
|
|
974
|
+
*/
|
|
975
|
+
/**
|
|
976
|
+
* The JSON document within a `live-plan -json` stdout: everything from the
|
|
977
|
+
* first line that is a bare `{` to the end. Returns the input unchanged when
|
|
978
|
+
* no such line exists.
|
|
979
|
+
*
|
|
980
|
+
* Defensive, and kept deliberately. choudoufu v0.14.0 keeps `-json` stdout to
|
|
981
|
+
* the document on every form of live root, so on a supported binary this
|
|
982
|
+
* returns its input unchanged and the slice never fires: it was written for
|
|
983
|
+
* the `-estate` path's refresh progress lines
|
|
984
|
+
* ([choudoufu #894](https://github.com/INTENTIUS/choudoufu/issues/894), fixed
|
|
985
|
+
* in PR 915, which routes every human-prose renderer of a `-json` run through
|
|
986
|
+
* `views.View.StdoutOnStderr()`). What it buys is that a future writer put
|
|
987
|
+
* back on stdout ahead of the document costs a stale slice rather than a
|
|
988
|
+
* `JSON.parse` failure, and a stdout carrying no document at all still fails
|
|
989
|
+
* in `JSON.parse` with the real text in the error.
|
|
990
|
+
*/
|
|
991
|
+
export function liveDocumentFrom(stdout: string): string {
|
|
992
|
+
const lines = stdout.split("\n");
|
|
993
|
+
const start = lines.findIndex((line) => line.trim() === "{");
|
|
994
|
+
return start === -1 ? stdout : lines.slice(start).join("\n");
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
export async function choudoufuLivePlan(
|
|
998
|
+
args: ChoudoufuLivePlanArgs & { documentPath?: string },
|
|
999
|
+
signal?: AbortSignal,
|
|
1000
|
+
): Promise<ChoudoufuLivePlanResult> {
|
|
1001
|
+
const resolved = await resolveRoot(args, signal);
|
|
1002
|
+
const { binary, dir } = resolved;
|
|
1003
|
+
const estate = resolveEstate(args, resolved, "choudoufuLivePlan");
|
|
1004
|
+
const env = terraformEnvironment(resolved.root);
|
|
1005
|
+
const documentPath = args.documentPath ?? DEFAULT_LIVE_PLAN_DOCUMENT_FILE;
|
|
1006
|
+
// A root that declares its own estate is run without `-estate`: choudoufu
|
|
1007
|
+
// refuses the flag beside a declaration and, since v0.14.0, settles the name
|
|
1008
|
+
// from the declaration itself. See {@link choudoufuLivePlanCommand}.
|
|
1009
|
+
const estateFlag: { estate?: string } = resolved.estate === undefined ? { estate } : {};
|
|
1010
|
+
|
|
1011
|
+
let drift: boolean;
|
|
1012
|
+
let jsonStdout: string;
|
|
1013
|
+
const planCmd = choudoufuLivePlanCommand({ binary, ...estateFlag, json: true });
|
|
1014
|
+
try {
|
|
1015
|
+
const { stdout, stderr } = await run(planCmd, dir, env, signal);
|
|
1016
|
+
report(stdout, stderr);
|
|
1017
|
+
drift = false;
|
|
1018
|
+
jsonStdout = stdout;
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
const failure = err as ExecFailure;
|
|
1021
|
+
if (typeof failure.code !== "number") throw err;
|
|
1022
|
+
if (failure.code !== 2) {
|
|
1023
|
+
const detail = (failure.stderr ?? "").trim() || (failure.stdout ?? "").trim();
|
|
1024
|
+
throw new Error(
|
|
1025
|
+
`${binary} live-plan failed in ${dir} (exit ${failure.code})${detail ? `\n${detail}` : ""}`,
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
report(failure.stdout ?? "", failure.stderr ?? "");
|
|
1029
|
+
drift = true;
|
|
1030
|
+
jsonStdout = failure.stdout ?? "";
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Belt and braces: v0.14.0 keeps `-json` stdout to the document, so this is
|
|
1034
|
+
// the identity on a supported binary. See {@link liveDocumentFrom}.
|
|
1035
|
+
const document = liveDocumentFrom(jsonStdout);
|
|
1036
|
+
const json: unknown = JSON.parse(document);
|
|
1037
|
+
// #788's document carries no render of the human plan itself
|
|
1038
|
+
// (`views.LivePlanDocument` has no such field), so the human text needs a
|
|
1039
|
+
// second live read rather than a field on the same response. `-adoption-only`
|
|
1040
|
+
// (#2105) changes which report that second read prints, never whether there
|
|
1041
|
+
// is one: choudoufu refuses the flag alongside `-json`, so the two reports
|
|
1042
|
+
// were always going to be two runs.
|
|
1043
|
+
const textCmd = choudoufuLivePlanCommand({
|
|
1044
|
+
binary,
|
|
1045
|
+
...estateFlag,
|
|
1046
|
+
json: false,
|
|
1047
|
+
noColor: true,
|
|
1048
|
+
...(args.adoptionOnly ? { adoptionOnly: true } : {}),
|
|
1049
|
+
});
|
|
1050
|
+
// The human render carries `-detailed-exitcode` too, so a plan with changes
|
|
1051
|
+
// comes back as exit 2 on this call exactly as it does on the `-json` one.
|
|
1052
|
+
// Both are success, and the drift answer is already settled above, so exit 2
|
|
1053
|
+
// here yields its stdout rather than throwing; anything else is a failure
|
|
1054
|
+
// with choudoufu's own output attached.
|
|
1055
|
+
let text: string;
|
|
1056
|
+
try {
|
|
1057
|
+
text = (await run(textCmd, dir, env, signal)).stdout;
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
const failure = err as ExecFailure;
|
|
1060
|
+
if (failure.code !== 2) {
|
|
1061
|
+
const detail = (failure.stderr ?? "").trim() || (failure.stdout ?? "").trim();
|
|
1062
|
+
throw new Error(
|
|
1063
|
+
`${binary} live-plan (human render) failed in ${dir} (exit ${String(failure.code)})` +
|
|
1064
|
+
`${detail ? `\n${detail}` : ""}`,
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
text = failure.stdout ?? "";
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
writeFileSync(join(dir, documentPath), document);
|
|
1071
|
+
|
|
1072
|
+
// The adoption commands only exist in the `-adoption-only` render, so a
|
|
1073
|
+
// candidate off an ordinary run carries the two marker values and no
|
|
1074
|
+
// command — which is the whole ownership contract either way.
|
|
1075
|
+
const ledger = readAdoptionLedger(json, args.adoptionOnly ? parseAdoptionCommands(text) : undefined);
|
|
1076
|
+
const ledgerText = renderAdoptionLedger(ledger, estate);
|
|
1077
|
+
|
|
1078
|
+
return {
|
|
1079
|
+
drift,
|
|
1080
|
+
json,
|
|
1081
|
+
text,
|
|
1082
|
+
ledger: ledgerText,
|
|
1083
|
+
finding: `${text.trimEnd()}\n\n${ledgerText}\n`,
|
|
1084
|
+
adoptions: ledger.adoptions,
|
|
1085
|
+
contested: ledger.contested,
|
|
1086
|
+
ambiguous: ledger.ambiguous,
|
|
1087
|
+
dir,
|
|
1088
|
+
documentPath,
|
|
1089
|
+
estate,
|
|
1090
|
+
...countLivePlanUnowned(json),
|
|
1091
|
+
...parseChoudoufuPlanSummary(text),
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* `choudoufu live-ls -estate=<estate> -json [-consistent]`: every resource
|
|
1097
|
+
* the account holds under the estate, read straight from the Resource Groups
|
|
1098
|
+
* Tagging API. No configuration, state or record store is read.
|
|
1099
|
+
*
|
|
1100
|
+
* Uses the fastIdempotent profile: like `terraformShow`, this reads an
|
|
1101
|
+
* artifact-shaped answer and does not plan.
|
|
1102
|
+
*/
|
|
1103
|
+
export async function choudoufuLiveLs(
|
|
1104
|
+
args: ChoudoufuLiveLsArgs,
|
|
1105
|
+
signal?: AbortSignal,
|
|
1106
|
+
): Promise<ChoudoufuLiveLsResult> {
|
|
1107
|
+
const resolved = await resolveRoot(args, signal);
|
|
1108
|
+
const { binary, dir } = resolved;
|
|
1109
|
+
const estate = resolveEstate(args, resolved, "choudoufuLiveLs");
|
|
1110
|
+
const env = terraformEnvironment(resolved.root);
|
|
1111
|
+
|
|
1112
|
+
const cmd = choudoufuLiveLsCommand({ binary, estate, ...(args.consistent ? { consistent: true } : {}) });
|
|
1113
|
+
const { stdout, stderr } = await run(cmd, dir, env, signal);
|
|
1114
|
+
report(stdout, stderr);
|
|
1115
|
+
|
|
1116
|
+
return { json: JSON.parse(stdout), dir, estate };
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Claim the adoptable matches an adoption ledger found, by writing the two
|
|
1121
|
+
* ownership markers onto each live resource (#2105).
|
|
1122
|
+
*
|
|
1123
|
+
* ## Why a tag write and not a policy verb
|
|
1124
|
+
*
|
|
1125
|
+
* choudoufu's `policy` block has an `adopt` verb for exactly this cell
|
|
1126
|
+
* (`declared_untagged`), and `site/content/docs/use/migrate.md` names
|
|
1127
|
+
* `policy { declared_untagged = "adopt" }` as the way to adopt a whole estate
|
|
1128
|
+
* at once instead of one resource at a time. That would have been the smaller
|
|
1129
|
+
* write: one apply, no tagging calls of chant's own. It is not reachable from
|
|
1130
|
+
* outside the configuration, and #2105 checked both ways in:
|
|
1131
|
+
*
|
|
1132
|
+
* - a `live` block in a terraform override file is dropped in silence.
|
|
1133
|
+
* `Module.mergeFile` (choudoufu's `internal/configs/module.go`) merges
|
|
1134
|
+
* backends, cloud blocks, providers and resources from an override file
|
|
1135
|
+
* and has no `Lives` case at all, so an `override.tf` carrying
|
|
1136
|
+
* `terraform { live { policy { declared_untagged = "adopt" } } }` changes
|
|
1137
|
+
* nothing. Measured, not inferred: a policy block with a deliberately
|
|
1138
|
+
* invalid verb in an override file passes `live-check` clean, and the same
|
|
1139
|
+
* block in a primary file is refused by name.
|
|
1140
|
+
* - a second primary file carrying one is refused outright: "A module may
|
|
1141
|
+
* have only one 'live' block." So is a sidecar beside an in-block form.
|
|
1142
|
+
*
|
|
1143
|
+
* Which leaves editing the root's own checked-in live configuration, running
|
|
1144
|
+
* an apply, and putting it back — a mutation of a reviewed file, with a
|
|
1145
|
+
* failure mode (a run that dies between the two edits) that leaves the estate
|
|
1146
|
+
* declaring "adopt everything declared and unmarked" for every later apply.
|
|
1147
|
+
* chant does not do that to a repository it does not own.
|
|
1148
|
+
*
|
|
1149
|
+
* So the mechanism is the tag write, which choudoufu's own migration guide
|
|
1150
|
+
* calls the whole contract: "There is no `choudoufu adopt` command and no need
|
|
1151
|
+
* for one. Two tags is the whole contract (`live/MARKERS.md`), so any tool
|
|
1152
|
+
* that writes two tags can adopt a resource." The command each write runs is
|
|
1153
|
+
* the paste-ready one choudoufu printed under `live-plan -adoption-only`,
|
|
1154
|
+
* carried on the candidate — built from choudoufu's own provider
|
|
1155
|
+
* configuration, so it lands on the region and endpoint the plan just read
|
|
1156
|
+
* rather than wherever an operator's AWS CLI profile points, and shell-quoted
|
|
1157
|
+
* by choudoufu for exactly this use.
|
|
1158
|
+
*
|
|
1159
|
+
* ## What it refuses
|
|
1160
|
+
*
|
|
1161
|
+
* A candidate with no printed command (a service whose tagging call this fork
|
|
1162
|
+
* does not spell out: IAM, Route53, S3) is refused with its two marker values
|
|
1163
|
+
* named, not guessed at. A contested address — more than one live resource at
|
|
1164
|
+
* one declared identity — is refused because no single tag write claims it;
|
|
1165
|
+
* `contested` is passed in so the Op's own result names what was refused
|
|
1166
|
+
* beside what was written, and nothing in this function can adopt one, since
|
|
1167
|
+
* `readAdoptionLedger` never puts a contested candidate in `adoptions`.
|
|
1168
|
+
*
|
|
1169
|
+
* Uses the longInfra profile: one tagging round trip per resource, against the
|
|
1170
|
+
* cloud.
|
|
1171
|
+
*/
|
|
1172
|
+
export async function choudoufuAdopt(
|
|
1173
|
+
args: ChoudoufuAdoptArgs,
|
|
1174
|
+
signal?: AbortSignal,
|
|
1175
|
+
): Promise<ChoudoufuAdoptResult> {
|
|
1176
|
+
const resolved = await resolveRoot(args, signal);
|
|
1177
|
+
const { dir } = resolved;
|
|
1178
|
+
const env = terraformEnvironment(resolved.root);
|
|
1179
|
+
const adoptions = args.adoptions ?? [];
|
|
1180
|
+
const contested = args.contested ?? [];
|
|
1181
|
+
|
|
1182
|
+
const adopted: string[] = [];
|
|
1183
|
+
const refused: AdoptionRefusal[] = [];
|
|
1184
|
+
|
|
1185
|
+
for (const candidate of contested) {
|
|
1186
|
+
refused.push({
|
|
1187
|
+
addr: candidate.addr,
|
|
1188
|
+
identity: candidate.identity,
|
|
1189
|
+
reason:
|
|
1190
|
+
"more than one live resource sits at this declared identity, so no single tag write claims it. " +
|
|
1191
|
+
`Delete the duplicate, or write tofu-estate=${candidate.markerEstate} ` +
|
|
1192
|
+
`tofu-address=${candidate.markerAddress} onto the one you mean.`,
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
for (const candidate of adoptions) {
|
|
1197
|
+
if (!candidate.command) {
|
|
1198
|
+
refused.push({
|
|
1199
|
+
addr: candidate.addr,
|
|
1200
|
+
identity: candidate.identity,
|
|
1201
|
+
reason:
|
|
1202
|
+
`${candidate.type} is tagged through its own service call, which choudoufu does not print a ` +
|
|
1203
|
+
`command for. Write tofu-estate=${candidate.markerEstate} ` +
|
|
1204
|
+
`tofu-address=${candidate.markerAddress} onto it with that call.`,
|
|
1205
|
+
});
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
const { stdout, stderr } = await run(candidate.command, dir, env, signal);
|
|
1209
|
+
report(stdout, stderr);
|
|
1210
|
+
adopted.push(candidate.addr);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
return {
|
|
1214
|
+
mechanism: "tag-write",
|
|
1215
|
+
adopted,
|
|
1216
|
+
adoptedCount: adopted.length,
|
|
1217
|
+
refused,
|
|
1218
|
+
ambiguous: new Set(contested.map((c) => c.addr)).size,
|
|
1219
|
+
dir,
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* `choudoufu live-check -json`: whether the configuration in this root can
|
|
1225
|
+
* move under live resource markers, and what stops it if it cannot. Makes no
|
|
1226
|
+
* cloud calls and needs no `live` block at all, since the check this activity
|
|
1227
|
+
* runs works on any configuration, which is why it needs no `estate`.
|
|
1228
|
+
*
|
|
1229
|
+
* A non-zero exit is a refusal choudoufu reports, not a failure of this
|
|
1230
|
+
* activity: `refused: true` carries it back rather than throwing. An abort
|
|
1231
|
+
* or a spawn failure (no numeric exit code) still throws.
|
|
1232
|
+
*
|
|
1233
|
+
* Uses the fastIdempotent profile: no cloud calls, no state.
|
|
1234
|
+
*/
|
|
1235
|
+
export async function choudoufuLiveCheck(
|
|
1236
|
+
args: ChoudoufuLiveCheckArgs,
|
|
1237
|
+
signal?: AbortSignal,
|
|
1238
|
+
): Promise<ChoudoufuLiveCheckResult> {
|
|
1239
|
+
const { binary, root, dir } = await resolveRoot(args, signal);
|
|
1240
|
+
const env = terraformEnvironment(root);
|
|
1241
|
+
const cmd = choudoufuLiveCheckCommand({ binary });
|
|
1242
|
+
|
|
1243
|
+
const parseOrUndefined = (text: string): unknown => {
|
|
1244
|
+
try {
|
|
1245
|
+
return JSON.parse(text);
|
|
1246
|
+
} catch {
|
|
1247
|
+
return undefined;
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
try {
|
|
1251
|
+
const { stdout, stderr } = await run(cmd, dir, env, signal);
|
|
1252
|
+
report(stdout, stderr);
|
|
1253
|
+
return { refused: false, json: parseOrUndefined(stdout), text: stdout, dir };
|
|
1254
|
+
} catch (err) {
|
|
1255
|
+
const failure = err as ExecFailure;
|
|
1256
|
+
if (typeof failure.code !== "number") throw err;
|
|
1257
|
+
const stdout = failure.stdout ?? "";
|
|
1258
|
+
report(stdout, failure.stderr ?? "");
|
|
1259
|
+
return { refused: true, json: parseOrUndefined(stdout), text: stdout, dir };
|
|
1260
|
+
}
|
|
1261
|
+
}
|