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