@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
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Live observation for declared terraform entities (#2087).
|
|
2
|
+
* Live observation for declared terraform entities (#2087, #2104).
|
|
3
3
|
*
|
|
4
4
|
* `buildRoots()` turns each configured root module into one entity per HCL
|
|
5
5
|
* block, keyed `<root>/<address>` (`./hcl/parse.ts`). This reader answers the
|
|
6
|
-
* lifecycle question for those entities
|
|
7
|
-
* the
|
|
6
|
+
* lifecycle question for those entities, and how it answers depends on one
|
|
7
|
+
* fact the parse already recorded on every entity: the root's `mode`.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* A **stock** root is read with `terraform show -json` over its state, and
|
|
10
|
+
* state membership is the ownership answer. A **live** root, meaning
|
|
11
|
+
* `terraform.binary` is `"choudoufu"` and the root declares an estate
|
|
12
|
+
* (#2103), has no state to show: it is read with `choudoufu live-plan -json`,
|
|
13
|
+
* and the marker on the resource is the ownership answer. The two halves are
|
|
14
|
+
* two adapters over the same `observeEntities` harness, picked per root.
|
|
15
|
+
*
|
|
16
|
+
* ## The state file is the ownership answer, on a stock root
|
|
10
17
|
*
|
|
11
18
|
* Every other lexicon in chant stamps a tag or a label at synthesis and reads
|
|
12
19
|
* it back off the live resource. Terraform stamps nothing, and there is
|
|
@@ -24,10 +31,58 @@
|
|
|
24
31
|
* escalates to a delete, which is the correct posture for a thing chant did
|
|
25
32
|
* not read a verdict for.
|
|
26
33
|
*
|
|
27
|
-
* That places terraform on the trusted-state-file row of the
|
|
28
|
-
* docs/src/content/docs/concepts/lifecycle-models.mdx, where
|
|
29
|
-
* sits on the live-marker row. `docs/pages/observation.mdx`
|
|
30
|
-
* version of this paragraph.
|
|
34
|
+
* That places a stock terraform root on the trusted-state-file row of the
|
|
35
|
+
* third axis in docs/src/content/docs/concepts/lifecycle-models.mdx, where
|
|
36
|
+
* chant elsewhere sits on the live-marker row. `docs/pages/observation.mdx`
|
|
37
|
+
* is the reader's version of this paragraph.
|
|
38
|
+
*
|
|
39
|
+
* ## The marker is the ownership answer, on a live root
|
|
40
|
+
*
|
|
41
|
+
* choudoufu keeps no authoritative state file. Every taggable resource it
|
|
42
|
+
* manages carries `tofu-estate` and `tofu-address` tags written in the create
|
|
43
|
+
* call, prior state is rebuilt from the live system each run, and the state
|
|
44
|
+
* file, where one exists at all, is a disposable cache. So a live root is on
|
|
45
|
+
* the live-marker row, the row every other chant lexicon sits in.
|
|
46
|
+
*
|
|
47
|
+
* chant does not re-derive that verdict from the tags. `live-plan -json`
|
|
48
|
+
* (choudoufu issue #788) already splits every declared instance four ways,
|
|
49
|
+
* and those four are the classification:
|
|
50
|
+
*
|
|
51
|
+
* | live-plan section | chant |
|
|
52
|
+
* |---|---|
|
|
53
|
+
* | `bound[]`, `source: "marker"` | present, `owned`; the marker named this estate and this address, and it is surfaced as `ResourceMetadata.marker` |
|
|
54
|
+
* | `bound[]`, any other source | present, `owned` by derivation, record, or cache; noted as such, and no marker is surfaced because none was read |
|
|
55
|
+
* | `unowned[]` with `adopt_*` | present, `unknown`; an adoptable match, carrying the exact two tag values #2105 would write |
|
|
56
|
+
* | `unowned[]` without | present, `foreign`; a live resource is in the way at a declared identity and the plan will not touch it |
|
|
57
|
+
* | `omissions[]` | not-observed, `ABSENT` excepted (see below) |
|
|
58
|
+
*
|
|
59
|
+
* The ownership channel this declares is `./live-ownership.ts`'s
|
|
60
|
+
* `TERRAFORM_LIVE_MARKER_KEYS`, and its module doc holds the record of why
|
|
61
|
+
* the keys are declared while the verdicts are set here by hand rather than
|
|
62
|
+
* core's `OwnershipChannel` growing a classifier for them.
|
|
63
|
+
*
|
|
64
|
+
* ### `ABSENT` is absence, and the rest of the omissions are not
|
|
65
|
+
*
|
|
66
|
+
* An omission is choudoufu saying an instance is missing from prior state,
|
|
67
|
+
* with a reason. Most of those reasons are claims of ignorance and become
|
|
68
|
+
* NOT-OBSERVED, which is what the tri-state exists for. One is not:
|
|
69
|
+
* `ABSENT` means "the instance has a usable import identity and the provider
|
|
70
|
+
* reported, normally, that no such object exists", which is exactly chant's
|
|
71
|
+
* OBSERVED-ABSENT, spelled "in neither map". Reporting it as not-observed
|
|
72
|
+
* instead would mean a live root could never propose a create for a resource
|
|
73
|
+
* that genuinely is not there yet, which is the opposite of the honesty the
|
|
74
|
+
* contract is for. `UNOWNED` is the other exception, in the other direction:
|
|
75
|
+
* the `unowned[]` section answers for that address with a real verdict, so
|
|
76
|
+
* the paired omission is never read on its own.
|
|
77
|
+
*
|
|
78
|
+
* ### A block's instances, aggregated
|
|
79
|
+
*
|
|
80
|
+
* chant's entity is the HCL block, `aws_eip.pool`; the document's rows are
|
|
81
|
+
* instances, `aws_eip.pool[0]`. So a block's verdict is the aggregate of its
|
|
82
|
+
* instances, in this precedence: any instance not-observed makes the block
|
|
83
|
+
* not-observed, then foreign, then adoptable, then owned, and a block whose
|
|
84
|
+
* instances are all absent is absent. Partial knowledge is not knowledge,
|
|
85
|
+
* which is the same rule choudoufu applies to its own `INCOMPLETE_BLOCK`.
|
|
31
86
|
*
|
|
32
87
|
* ## What is readable at all
|
|
33
88
|
*
|
|
@@ -37,13 +92,28 @@
|
|
|
37
92
|
* That is honest, not a gap to close by inventing an address for something
|
|
38
93
|
* that has none.
|
|
39
94
|
*
|
|
95
|
+
* A live root reads one block narrower. `live-plan`'s document is prior state
|
|
96
|
+
* for managed resources, so a `data` block has no row in it either and reads
|
|
97
|
+
* `unsupported-kind` there, where the stock reader can answer for it.
|
|
98
|
+
*
|
|
40
99
|
* ## Tri-state (#1089)
|
|
41
100
|
*
|
|
42
|
-
* A root whose `init` or `show` fails
|
|
43
|
-
* root as not-observed with
|
|
44
|
-
* absent. A failed read must never render as a list
|
|
45
|
-
* read independently and merged (`mergeObservations`),
|
|
46
|
-
* does not un-observe a root that answered.
|
|
101
|
+
* A root whose `init` or `show` fails, or whose `live-plan` fails, reports
|
|
102
|
+
* EVERY entity declared in that root as not-observed with the mapped reason
|
|
103
|
+
* and the root named, never absent. A failed read must never render as a list
|
|
104
|
+
* of creates. Roots are read independently and merged (`mergeObservations`),
|
|
105
|
+
* so one broken backend does not un-observe a root that answered.
|
|
106
|
+
*
|
|
107
|
+
* ## Beyond the declared estate, on a live root
|
|
108
|
+
*
|
|
109
|
+
* `live-ls -estate -json` lists the whole estate off the Resource Groups
|
|
110
|
+
* Tagging API with no configuration read at all, which is a question a state
|
|
111
|
+
* file cannot answer: what does this estate hold that nothing declares? That
|
|
112
|
+
* is what makes `ambientKinds()`, `observeAmbient()` and `teardownOwned()`
|
|
113
|
+
* implementable on this lexicon for the first time. `-consistent` is always
|
|
114
|
+
* passed, because that index lags a tag write by about a minute
|
|
115
|
+
* (`live-ls`'s own help text) and a listing taken right after an apply can
|
|
116
|
+
* otherwise show a resource under both its old and new estate, or neither.
|
|
47
117
|
*
|
|
48
118
|
* ## Nothing from `values` is surfaced
|
|
49
119
|
*
|
|
@@ -54,7 +124,13 @@
|
|
|
54
124
|
* `id`, which is the physical id every observation carries.
|
|
55
125
|
*/
|
|
56
126
|
|
|
57
|
-
import type {
|
|
127
|
+
import type {
|
|
128
|
+
DescribeResourcesResult,
|
|
129
|
+
ResourceMetadata,
|
|
130
|
+
TeardownCandidate,
|
|
131
|
+
TeardownEnumeration,
|
|
132
|
+
TeardownHole,
|
|
133
|
+
} from "@intentius/chant/lexicon";
|
|
58
134
|
import {
|
|
59
135
|
mergeObservations,
|
|
60
136
|
normalizeObservation,
|
|
@@ -63,14 +139,22 @@ import {
|
|
|
63
139
|
type DeclaredEntity,
|
|
64
140
|
type EntityObservation,
|
|
65
141
|
type ObserverAdapter,
|
|
142
|
+
type UnobservedReason,
|
|
66
143
|
} from "@intentius/chant/observation";
|
|
67
|
-
import {
|
|
68
|
-
import {
|
|
144
|
+
import type { OwnershipMarker } from "@intentius/chant/ownership";
|
|
145
|
+
import {
|
|
146
|
+
choudoufuLiveLs,
|
|
147
|
+
choudoufuLivePlan,
|
|
148
|
+
terraformInit,
|
|
149
|
+
terraformShow,
|
|
150
|
+
} from "./op/activities/terraform";
|
|
151
|
+
import { DATA_TYPE, MODULE_TYPE, RESOURCE_TYPE, callersOfKey } from "./hcl/parse";
|
|
69
152
|
|
|
70
153
|
// The channel keys live in their own module so `plugin.ts` can declare
|
|
71
154
|
// `ownershipChannel` without loading this reader. Re-exported here because
|
|
72
155
|
// this is where they are used.
|
|
73
156
|
export { TERRAFORM_STATE_OWNERSHIP_KEYS } from "./state-ownership";
|
|
157
|
+
export { TERRAFORM_LIVE_MARKER_KEYS } from "./live-ownership";
|
|
74
158
|
|
|
75
159
|
/** One row out of `values.root_module.resources[]`, at any module depth. */
|
|
76
160
|
export interface StateResourceRow {
|
|
@@ -165,40 +249,724 @@ function moduleIsLive(address: string, index: StateIndex): boolean {
|
|
|
165
249
|
return false;
|
|
166
250
|
}
|
|
167
251
|
|
|
168
|
-
|
|
252
|
+
/* ─────────────────────────── live roots (#2104) ─────────────────────────── */
|
|
253
|
+
|
|
254
|
+
/** One `bound[]` entry: a declared instance the plan admitted into prior state. */
|
|
255
|
+
export interface LivePlanBoundRow {
|
|
256
|
+
addr: string;
|
|
257
|
+
type?: string;
|
|
258
|
+
/** The import id it bound to, when the resolver produced one. */
|
|
259
|
+
identity?: string;
|
|
260
|
+
/** Which admission path supplied the identity: `marker`, `record`, `derived` or `cache`. */
|
|
261
|
+
source?: string;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** One `omissions[]` entry: a declared instance the plan could not read, and why. */
|
|
265
|
+
export interface LivePlanOmissionRow {
|
|
266
|
+
addr: string;
|
|
267
|
+
/** choudoufu's own machine-readable code (`ABSENT`, `FAILED`, `UNOWNED`, …). */
|
|
268
|
+
reason: string;
|
|
269
|
+
detail: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** One `unowned[]` entry: a live resource at a declared identity without this estate's marker. */
|
|
273
|
+
export interface LivePlanUnownedRow {
|
|
274
|
+
addr: string;
|
|
275
|
+
type?: string;
|
|
276
|
+
identity?: string;
|
|
277
|
+
/** `tofu_estate`: the marker it does carry, when it carries one. Empty means no marker at all. */
|
|
278
|
+
heldBy?: string;
|
|
279
|
+
/** `adopt_tofu_estate`: present exactly when adoption is this run's to offer. */
|
|
280
|
+
adoptEstate?: string;
|
|
281
|
+
/** `adopt_tofu_address`: the escaped address the adopting tag write would carry. */
|
|
282
|
+
adoptAddress?: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** A `live-plan -json` document, indexed by declared instance address. */
|
|
286
|
+
export interface LivePlanIndex {
|
|
287
|
+
/** `estate`: the estate every section was computed against. */
|
|
288
|
+
estate: string;
|
|
289
|
+
bound: Map<string, LivePlanBoundRow>;
|
|
290
|
+
omissions: Map<string, LivePlanOmissionRow>;
|
|
291
|
+
unowned: Map<string, LivePlanUnownedRow>;
|
|
292
|
+
/** Every address any section named, so a block can find its own instances. */
|
|
293
|
+
addresses: string[];
|
|
294
|
+
/** `diagnostics[]` summaries, for the observation's run-level notes. */
|
|
295
|
+
diagnostics: string[];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Index choudoufu issue #788's document. Unknown fields are ignored and a
|
|
300
|
+
* missing section reads as empty: a document from a newer choudoufu must
|
|
301
|
+
* degrade to "this section said nothing", never to a throw.
|
|
302
|
+
*/
|
|
303
|
+
export function indexLivePlan(document: unknown): LivePlanIndex {
|
|
304
|
+
const doc = asRecord(document);
|
|
305
|
+
const index: LivePlanIndex = {
|
|
306
|
+
estate: asString(doc.estate) ?? "",
|
|
307
|
+
bound: new Map(),
|
|
308
|
+
omissions: new Map(),
|
|
309
|
+
unowned: new Map(),
|
|
310
|
+
addresses: [],
|
|
311
|
+
diagnostics: [],
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
for (const entry of asArray(doc.bound)) {
|
|
315
|
+
const row = asRecord(entry);
|
|
316
|
+
const addr = asString(row.addr);
|
|
317
|
+
if (!addr) continue;
|
|
318
|
+
index.bound.set(addr, {
|
|
319
|
+
addr,
|
|
320
|
+
...(asString(row.type) ? { type: asString(row.type) } : {}),
|
|
321
|
+
...(asString(row.identity) ? { identity: asString(row.identity) } : {}),
|
|
322
|
+
...(asString(row.source) ? { source: asString(row.source) } : {}),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for (const entry of asArray(doc.omissions)) {
|
|
327
|
+
const row = asRecord(entry);
|
|
328
|
+
const addr = asString(row.addr);
|
|
329
|
+
if (!addr) continue;
|
|
330
|
+
index.omissions.set(addr, {
|
|
331
|
+
addr,
|
|
332
|
+
reason: asString(row.reason) ?? "",
|
|
333
|
+
detail: asString(row.detail) ?? "",
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
for (const entry of asArray(doc.unowned)) {
|
|
338
|
+
const row = asRecord(entry);
|
|
339
|
+
const addr = asString(row.addr);
|
|
340
|
+
if (!addr) continue;
|
|
341
|
+
index.unowned.set(addr, {
|
|
342
|
+
addr,
|
|
343
|
+
...(asString(row.type) ? { type: asString(row.type) } : {}),
|
|
344
|
+
...(asString(row.identity) ? { identity: asString(row.identity) } : {}),
|
|
345
|
+
...(asString(row.tofu_estate) ? { heldBy: asString(row.tofu_estate) } : {}),
|
|
346
|
+
...(asString(row.adopt_tofu_estate) ? { adoptEstate: asString(row.adopt_tofu_estate) } : {}),
|
|
347
|
+
...(asString(row.adopt_tofu_address) ? { adoptAddress: asString(row.adopt_tofu_address) } : {}),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
for (const entry of asArray(doc.diagnostics)) {
|
|
352
|
+
const row = asRecord(entry);
|
|
353
|
+
const summary = asString(row.summary);
|
|
354
|
+
if (summary) index.diagnostics.push(`${asString(row.severity) ?? "warning"}: ${summary}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
index.addresses = [
|
|
358
|
+
...new Set([...index.bound.keys(), ...index.omissions.keys(), ...index.unowned.keys()]),
|
|
359
|
+
].sort();
|
|
360
|
+
return index;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* choudoufu's omission reason codes, mapped onto chant's total vocabulary
|
|
365
|
+
* (`internal/live/projection/result.go`). `"absent"` is not an
|
|
366
|
+
* {@link UnobservedReason}: it is the tri-state's OBSERVED-ABSENT, the one
|
|
367
|
+
* omission that is a claim of knowledge rather than of ignorance.
|
|
368
|
+
*
|
|
369
|
+
* A code this table does not know maps to `read-failed`, which is the safe
|
|
370
|
+
* direction: no NOT-OBSERVED verdict ever becomes a create or a delete.
|
|
371
|
+
*/
|
|
372
|
+
export const LIVE_PLAN_OMISSION_REASONS: Readonly<Record<string, UnobservedReason | "absent">> = {
|
|
373
|
+
// The provider was asked and reported, normally, that nothing is there.
|
|
374
|
+
ABSENT: "absent",
|
|
375
|
+
// The provider errored, or could not be reached in a usable order.
|
|
376
|
+
FAILED: "read-failed",
|
|
377
|
+
PARENT_UNAVAILABLE: "read-failed",
|
|
378
|
+
CYCLE: "read-failed",
|
|
379
|
+
INCOMPLETE_BLOCK: "read-failed",
|
|
380
|
+
LISTED_NOT_IMPORTABLE: "read-failed",
|
|
381
|
+
// Something exists at the identity and does not carry this estate's marker.
|
|
382
|
+
// Normally answered by the `unowned[]` section before this table is read;
|
|
383
|
+
// this entry is what happens if a document ever carries the omission alone.
|
|
384
|
+
UNOWNED: "read-failed",
|
|
385
|
+
// The mode cannot name, list or tag this instance at all: a server-assigned
|
|
386
|
+
// identity with no marker to find it by, a resource with no cloud object to
|
|
387
|
+
// read, or an address another declared instance has taken over.
|
|
388
|
+
NEEDS_DISCOVERY: "unsupported-kind",
|
|
389
|
+
UNREADABLE: "unsupported-kind",
|
|
390
|
+
SUPERSEDED: "unsupported-kind",
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
/** Does this text name a credentials or authorization failure rather than any other error? */
|
|
394
|
+
function readsAsCredentials(text: string): boolean {
|
|
395
|
+
return /\b(no valid credential|credentials?|not authorized|unauthorized|accessdenied|access denied|expiredtoken|invalidclienttokenid|signature)\b/i.test(
|
|
396
|
+
text,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** The chant verdict for one omission: `no-credentials` when its detail says so. */
|
|
401
|
+
function omissionVerdict(row: LivePlanOmissionRow): UnobservedReason | "absent" {
|
|
402
|
+
const mapped = LIVE_PLAN_OMISSION_REASONS[row.reason] ?? "read-failed";
|
|
403
|
+
if (mapped === "read-failed" && readsAsCredentials(row.detail)) return "no-credentials";
|
|
404
|
+
return mapped;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Every instance address in the document belonging to the declared block `address`. */
|
|
408
|
+
function instancesOf(address: string, index: LivePlanIndex): string[] {
|
|
409
|
+
return index.addresses.filter((a) => a === address || a.startsWith(`${address}[`));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** True when the document names any instance under `module.<name>`, at any depth. */
|
|
413
|
+
function liveModuleMembers(address: string, index: LivePlanIndex): string[] {
|
|
414
|
+
return index.addresses.filter(
|
|
415
|
+
(a) => a === address || a.startsWith(`${address}.`) || a.startsWith(`${address}[`),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** One instance's verdict, before a block aggregates its instances. */
|
|
420
|
+
type InstanceVerdict =
|
|
421
|
+
| { kind: "owned"; row: LivePlanBoundRow }
|
|
422
|
+
| { kind: "adoptable"; row: LivePlanUnownedRow }
|
|
423
|
+
| { kind: "foreign"; row: LivePlanUnownedRow }
|
|
424
|
+
| { kind: "absent" }
|
|
425
|
+
| { kind: "unobserved"; reason: UnobservedReason; detail: string };
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Classify one instance address against the document, `unowned[]` first: a
|
|
429
|
+
* declared instance that also carries a `UNOWNED` omission is answered by the
|
|
430
|
+
* section that has the verdict, not by the one that has the apology.
|
|
431
|
+
*/
|
|
432
|
+
function classifyLiveInstance(address: string, index: LivePlanIndex): InstanceVerdict {
|
|
433
|
+
const unowned = index.unowned.get(address);
|
|
434
|
+
if (unowned) {
|
|
435
|
+
return unowned.adoptEstate || unowned.adoptAddress
|
|
436
|
+
? { kind: "adoptable", row: unowned }
|
|
437
|
+
: { kind: "foreign", row: unowned };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const bound = index.bound.get(address);
|
|
441
|
+
if (bound) return { kind: "owned", row: bound };
|
|
442
|
+
|
|
443
|
+
const omission = index.omissions.get(address);
|
|
444
|
+
if (omission) {
|
|
445
|
+
const verdict = omissionVerdict(omission);
|
|
446
|
+
if (verdict === "absent") return { kind: "absent" };
|
|
447
|
+
return {
|
|
448
|
+
kind: "unobserved",
|
|
449
|
+
reason: verdict,
|
|
450
|
+
detail: `${address}: ${omission.reason}${omission.detail ? ` — ${omission.detail}` : ""}`,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return { kind: "absent" };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* The declared block's verdict, aggregated over its instances in the
|
|
459
|
+
* precedence the module doc states: not-observed, then foreign, then
|
|
460
|
+
* adoptable, then owned, and all-absent last. A block whose instances are
|
|
461
|
+
* only partly known is not partly owned; it is unknown, which never becomes
|
|
462
|
+
* a delete.
|
|
463
|
+
*/
|
|
464
|
+
function readLiveResource(
|
|
465
|
+
entity: TerraformDeclared,
|
|
466
|
+
index: LivePlanIndex,
|
|
467
|
+
root: string,
|
|
468
|
+
queried: string,
|
|
469
|
+
): EntityObservation {
|
|
470
|
+
const { address } = entity;
|
|
471
|
+
const instances = instancesOf(address, index);
|
|
472
|
+
if (instances.length === 0) return { absent: true, queried };
|
|
473
|
+
|
|
474
|
+
const verdicts = instances.map((a) => classifyLiveInstance(a, index));
|
|
475
|
+
const attributes: Record<string, unknown> = { address, root, estate: index.estate };
|
|
476
|
+
if (instances.length > 1 || instances[0] !== address) attributes.instances = instances;
|
|
477
|
+
|
|
478
|
+
const unobserved = verdicts.find((v) => v.kind === "unobserved");
|
|
479
|
+
if (unobserved && unobserved.kind === "unobserved") {
|
|
480
|
+
return { unobserved: { reason: unobserved.reason, detail: unobserved.detail }, queried };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const foreign = verdicts.find((v) => v.kind === "foreign");
|
|
484
|
+
if (foreign && foreign.kind === "foreign") {
|
|
485
|
+
return {
|
|
486
|
+
present: {
|
|
487
|
+
type: entity.type,
|
|
488
|
+
physicalId: foreign.row.identity ?? address,
|
|
489
|
+
status: "unowned",
|
|
490
|
+
ownership: "foreign",
|
|
491
|
+
attributes: {
|
|
492
|
+
...attributes,
|
|
493
|
+
...(foreign.row.type ? { resourceType: foreign.row.type } : {}),
|
|
494
|
+
...(foreign.row.heldBy ? { heldBy: foreign.row.heldBy } : {}),
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
queried,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const adoptable = verdicts.find((v) => v.kind === "adoptable");
|
|
502
|
+
if (adoptable && adoptable.kind === "adoptable") {
|
|
503
|
+
// `unknown`, not `foreign`: the resource matches the declaration and one
|
|
504
|
+
// tag write claims it. The two values that write carries are the whole
|
|
505
|
+
// adoption proposal #2105 builds its Op on, so they ride the metadata
|
|
506
|
+
// rather than being re-derived from the address there. No `marker` is
|
|
507
|
+
// surfaced, because nothing was read off this resource: it carries none.
|
|
508
|
+
return {
|
|
509
|
+
present: {
|
|
510
|
+
type: entity.type,
|
|
511
|
+
physicalId: adoptable.row.identity ?? address,
|
|
512
|
+
status: "adoptable",
|
|
513
|
+
ownership: "unknown",
|
|
514
|
+
attributes: {
|
|
515
|
+
...attributes,
|
|
516
|
+
...(adoptable.row.type ? { resourceType: adoptable.row.type } : {}),
|
|
517
|
+
...(adoptable.row.adoptEstate ? { adoptTofuEstate: adoptable.row.adoptEstate } : {}),
|
|
518
|
+
...(adoptable.row.adoptAddress ? { adoptTofuAddress: adoptable.row.adoptAddress } : {}),
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
queried,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const owned = verdicts.filter((v) => v.kind === "owned") as Array<{ kind: "owned"; row: LivePlanBoundRow }>;
|
|
526
|
+
if (owned.length === 0) return { absent: true, queried };
|
|
527
|
+
|
|
528
|
+
const sources = [...new Set(owned.map((v) => v.row.source ?? "unknown"))].sort();
|
|
529
|
+
const byMarker = sources.length === 1 && sources[0] === "marker";
|
|
530
|
+
return {
|
|
531
|
+
present: {
|
|
532
|
+
type: entity.type,
|
|
533
|
+
physicalId: owned.length === 1 ? (owned[0]!.row.identity ?? address) : address,
|
|
534
|
+
status: "bound",
|
|
535
|
+
ownership: "owned",
|
|
536
|
+
// The marker is surfaced only where one was actually read. A `derived`
|
|
537
|
+
// bind computed the identity from the configuration and looked at no
|
|
538
|
+
// tag, so claiming a marker for it would be a guess.
|
|
539
|
+
...(byMarker ? { marker: { stack: index.estate } as OwnershipMarker } : {}),
|
|
540
|
+
attributes: {
|
|
541
|
+
...attributes,
|
|
542
|
+
...(owned[0]!.row.type ? { resourceType: owned[0]!.row.type } : {}),
|
|
543
|
+
boundBy: sources.join(","),
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
queried,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* One live root's reader: `live-plan -json`, then address matching against
|
|
552
|
+
* the document. `terraform show` is never run here, and there is no state
|
|
553
|
+
* file to run it over.
|
|
554
|
+
*/
|
|
555
|
+
function liveAdapter(root: string, cwd: string | undefined, deps: TerraformReadDeps): ObserverAdapter<LivePlanIndex> {
|
|
556
|
+
const where = cwd ? { cwd } : {};
|
|
557
|
+
let dir: string | undefined;
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
async bind(): Promise<LivePlanIndex> {
|
|
561
|
+
const planned = await deps.livePlan({ root, ...where });
|
|
562
|
+
dir = planned.dir;
|
|
563
|
+
return indexLivePlan(planned.json);
|
|
564
|
+
},
|
|
565
|
+
|
|
566
|
+
classifyBindFailure(err) {
|
|
567
|
+
const message = err instanceof Error ? err.message.split("\n")[0] : String(err);
|
|
568
|
+
const full = err instanceof Error ? err.message : String(err);
|
|
569
|
+
return {
|
|
570
|
+
reason: readsAsCredentials(full) ? "no-credentials" : "read-failed",
|
|
571
|
+
detail: `terraform.roots.${root}${dir ? ` (${dir})` : ""}: ${message}`,
|
|
572
|
+
};
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
async read(index, entity): Promise<EntityObservation> {
|
|
576
|
+
const declared = entity as TerraformDeclared;
|
|
577
|
+
const { address } = declared;
|
|
578
|
+
const queried = `choudoufu live-plan -json (root "${root}", estate "${index.estate}", address "${address}")`;
|
|
579
|
+
|
|
580
|
+
if (entity.type === RESOURCE_TYPE) return readLiveResource(declared, index, root, queried);
|
|
581
|
+
|
|
582
|
+
if (entity.type === DATA_TYPE) {
|
|
583
|
+
return {
|
|
584
|
+
unobserved: {
|
|
585
|
+
reason: "unsupported-kind",
|
|
586
|
+
detail:
|
|
587
|
+
"live-plan's document is prior state for managed resources; a data block has no row in bound, omissions or unowned",
|
|
588
|
+
},
|
|
589
|
+
queried,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (entity.type === MODULE_TYPE) {
|
|
594
|
+
const members = liveModuleMembers(address, index);
|
|
595
|
+
if (members.length === 0) return { absent: true, queried };
|
|
596
|
+
// Same shape as the stock reader's module answer, for the same
|
|
597
|
+
// reason: the document names the module's resources and never the
|
|
598
|
+
// block itself, so the ownership channel has nothing to say about it.
|
|
599
|
+
return {
|
|
600
|
+
present: {
|
|
601
|
+
type: entity.type,
|
|
602
|
+
physicalId: address,
|
|
603
|
+
status: "module",
|
|
604
|
+
ownership: "unknown",
|
|
605
|
+
attributes: { address, root, estate: index.estate, instances: members },
|
|
606
|
+
},
|
|
607
|
+
queried,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
unobserved: {
|
|
613
|
+
reason: "unsupported-kind",
|
|
614
|
+
detail: `${entity.type} has no row in a live-plan document: only resource blocks do`,
|
|
615
|
+
},
|
|
616
|
+
queried,
|
|
617
|
+
};
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/* ──────────────────── the estate beyond the declaration ─────────────────── */
|
|
623
|
+
|
|
624
|
+
/** One `items[]` entry of a `live-ls -json` listing. */
|
|
625
|
+
export interface LiveLsItem {
|
|
626
|
+
/** ARN or other stable identity. */
|
|
627
|
+
id: string;
|
|
628
|
+
/** Resource type, e.g. `aws_vpc`. */
|
|
629
|
+
type: string;
|
|
630
|
+
/** The configuration address decoded from the marker, when the listing could decode one. */
|
|
631
|
+
address?: string;
|
|
632
|
+
/** `tofu-slot`, on a `count` instance. */
|
|
633
|
+
slot?: string;
|
|
634
|
+
/** Whether the cross-referenced configuration directory still declares it. */
|
|
635
|
+
declared: boolean;
|
|
636
|
+
/** Which read found it: `tagging`, or a per-service pass. */
|
|
637
|
+
source?: string;
|
|
638
|
+
/** Every marker tag it carries. */
|
|
639
|
+
tags: Record<string, string>;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** One `gaps[]` entry: a declared instance this listing's mechanism cannot reach. */
|
|
643
|
+
export interface LiveLsGap {
|
|
644
|
+
address: string;
|
|
645
|
+
type: string;
|
|
646
|
+
/** `record` or `declaration-carried`, choudoufu's own rung vocabulary. */
|
|
647
|
+
rung: string;
|
|
648
|
+
detail: string;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** A `live-ls -json` listing, parsed. */
|
|
652
|
+
export interface LiveLsListing {
|
|
653
|
+
estate: string;
|
|
654
|
+
items: LiveLsItem[];
|
|
655
|
+
gaps: LiveLsGap[];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** Parse a `live-ls -json` document. A missing section reads as empty, never as a throw. */
|
|
659
|
+
export function readLiveLs(document: unknown): LiveLsListing {
|
|
660
|
+
const doc = asRecord(document);
|
|
661
|
+
const items: LiveLsItem[] = [];
|
|
662
|
+
for (const entry of asArray(doc.items)) {
|
|
663
|
+
const row = asRecord(entry);
|
|
664
|
+
const id = asString(row.id);
|
|
665
|
+
const type = asString(row.type);
|
|
666
|
+
if (!id || !type) continue;
|
|
667
|
+
const tags: Record<string, string> = {};
|
|
668
|
+
for (const [key, value] of Object.entries(asRecord(row.tags))) {
|
|
669
|
+
if (typeof value === "string") tags[key] = value;
|
|
670
|
+
}
|
|
671
|
+
items.push({
|
|
672
|
+
id,
|
|
673
|
+
type,
|
|
674
|
+
...(asString(row.address) ? { address: asString(row.address) } : {}),
|
|
675
|
+
...(asString(row.slot) ? { slot: asString(row.slot) } : {}),
|
|
676
|
+
declared: row.declared === true,
|
|
677
|
+
...(asString(row.source) ? { source: asString(row.source) } : {}),
|
|
678
|
+
tags,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const gaps: LiveLsGap[] = [];
|
|
683
|
+
for (const entry of asArray(doc.gaps)) {
|
|
684
|
+
const row = asRecord(entry);
|
|
685
|
+
const address = asString(row.address);
|
|
686
|
+
if (!address) continue;
|
|
687
|
+
gaps.push({
|
|
688
|
+
address,
|
|
689
|
+
type: asString(row.type) ?? "",
|
|
690
|
+
rung: asString(row.rung) ?? "",
|
|
691
|
+
detail: asString(row.detail) ?? "",
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return { estate: asString(doc.estate) ?? "", items, gaps };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* A live-ls gap's rung, mapped onto chant's vocabulary. Both rungs mean the
|
|
700
|
+
* same thing to a reader of the listing: this instance can never appear in
|
|
701
|
+
* it, because the mechanism the listing uses cannot reach it. That is
|
|
702
|
+
* `unsupported-kind`, not a failure and not an absence.
|
|
703
|
+
*/
|
|
704
|
+
function gapReason(): UnobservedReason {
|
|
705
|
+
return "unsupported-kind";
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* The entity key `buildRoots()` would have produced for a live resource's
|
|
710
|
+
* address.
|
|
711
|
+
*
|
|
712
|
+
* A live address is fully qualified with dots, `module.cdn.null_resource.edge`;
|
|
713
|
+
* the build keys a descended child module's block with a slash per call,
|
|
714
|
+
* `app/module.cdn/null_resource.edge` (#2112). Splitting the `module.<name>.`
|
|
715
|
+
* prefixes back out is what keeps a live row and the block that declared it
|
|
716
|
+
* on the same key.
|
|
717
|
+
*/
|
|
718
|
+
export function entityKeyFor(root: string, address: string): string {
|
|
719
|
+
const parts = address.split(".");
|
|
720
|
+
const segments: string[] = [];
|
|
721
|
+
let i = 0;
|
|
722
|
+
while (i + 1 < parts.length && parts[i] === "module") {
|
|
723
|
+
segments.push(`module.${parts[i + 1]}`);
|
|
724
|
+
i += 2;
|
|
725
|
+
}
|
|
726
|
+
segments.push(parts.slice(i).join("."));
|
|
727
|
+
return [root, ...segments].join("/");
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* The entity types this lexicon can enumerate beyond the declared estate
|
|
732
|
+
* (#1278). One: a `resource` block, whose live counterparts `live-ls` lists
|
|
733
|
+
* off the tag index. A `data`, `module` or `provider` block has no live
|
|
734
|
+
* counterpart to enumerate at all.
|
|
735
|
+
*/
|
|
736
|
+
export function ambientKinds(): string[] {
|
|
737
|
+
return [RESOURCE_TYPE];
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** Shared options for the two estate-wide reads. */
|
|
741
|
+
export interface LiveEstateOptions {
|
|
742
|
+
/** Directory the activities start the `chant.config.*` search from. Default: the process cwd. */
|
|
743
|
+
cwd?: string;
|
|
744
|
+
/** Restrict to these root names. Default: every live root the project declares. */
|
|
745
|
+
roots?: string[];
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Every live root the project declares, in config order. Reads the project
|
|
750
|
+
* config the same way the activities do, so `terraform.roots` is read once
|
|
751
|
+
* and the same answer reaches both.
|
|
752
|
+
*/
|
|
753
|
+
export async function liveRootNames(cwd?: string): Promise<Array<{ root: string; estate: string }>> {
|
|
754
|
+
const { loadChantConfigUpward } = await import("@intentius/chant/config");
|
|
755
|
+
const { detectLiveEstate } = await import("./op/activities/live-detect");
|
|
756
|
+
const { dirname, resolve } = await import("node:path");
|
|
757
|
+
|
|
758
|
+
const start = resolve(cwd ?? process.cwd());
|
|
759
|
+
const { config, configPath } = await loadChantConfigUpward(start);
|
|
760
|
+
const projectRoot = configPath ? dirname(configPath) : start;
|
|
761
|
+
const namespace = (config as { terraform?: { binary?: string; roots?: Record<string, { dir: string }> } })
|
|
762
|
+
.terraform;
|
|
763
|
+
if (namespace?.binary !== "choudoufu") return [];
|
|
764
|
+
|
|
765
|
+
const live: Array<{ root: string; estate: string }> = [];
|
|
766
|
+
for (const [name, root] of Object.entries(namespace.roots ?? {})) {
|
|
767
|
+
const estate = detectLiveEstate(resolve(projectRoot, root.dir));
|
|
768
|
+
if (estate !== undefined) live.push({ root: name, estate });
|
|
769
|
+
}
|
|
770
|
+
return live;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Report the resources this estate owns that nothing declares (#1278) — the
|
|
775
|
+
* owned-orphan set, which is `live-ls`'s `declared: false` rows.
|
|
776
|
+
*
|
|
777
|
+
* This is the question a state file cannot answer at all, because a state
|
|
778
|
+
* file knows only what it created. The tag index knows what the estate holds,
|
|
779
|
+
* whether or not any configuration still mentions it.
|
|
780
|
+
*/
|
|
781
|
+
export async function observeAmbient(
|
|
782
|
+
options: {
|
|
783
|
+
environment: string;
|
|
784
|
+
kinds: string[];
|
|
785
|
+
observed: Record<string, ResourceMetadata>;
|
|
786
|
+
} & LiveEstateOptions,
|
|
787
|
+
deps: TerraformReadDeps = REAL_DEPS,
|
|
788
|
+
): Promise<Record<string, ResourceMetadata>> {
|
|
789
|
+
// The bound #1278 asks for: a project that declares no terraform resources
|
|
790
|
+
// is never made to enumerate an estate.
|
|
791
|
+
if (!options.kinds.includes(RESOURCE_TYPE)) return {};
|
|
792
|
+
|
|
793
|
+
const out: Record<string, ResourceMetadata> = {};
|
|
794
|
+
for (const { root, estate } of await liveRoots(options)) {
|
|
795
|
+
const listed = await deps.liveLs({
|
|
796
|
+
root,
|
|
797
|
+
consistent: true,
|
|
798
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
799
|
+
});
|
|
800
|
+
const listing = readLiveLs(listed.json);
|
|
801
|
+
for (const item of listing.items) {
|
|
802
|
+
if (item.declared) continue; // declared is `describeResources`'s business, not this one's
|
|
803
|
+
const address = item.address ?? item.id;
|
|
804
|
+
const name = entityKeyFor(root, address);
|
|
805
|
+
if (options.observed[name]) continue;
|
|
806
|
+
out[name] = {
|
|
807
|
+
type: RESOURCE_TYPE,
|
|
808
|
+
physicalId: item.id,
|
|
809
|
+
status: "orphan",
|
|
810
|
+
ownership: "owned",
|
|
811
|
+
marker: { stack: listing.estate || estate },
|
|
812
|
+
ambient: true,
|
|
813
|
+
attributes: {
|
|
814
|
+
address,
|
|
815
|
+
root,
|
|
816
|
+
estate: listing.estate || estate,
|
|
817
|
+
resourceType: item.type,
|
|
818
|
+
...(item.slot ? { slot: item.slot } : {}),
|
|
819
|
+
...(item.source ? { listedBy: item.source } : {}),
|
|
820
|
+
tags: item.tags,
|
|
821
|
+
},
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return out;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Name what a teardown of this environment would remove from every live root
|
|
830
|
+
* (#1222): the estate's owned orphans, which are exactly the set choudoufu's
|
|
831
|
+
* own default `policy` verb (`undeclared_tagged = "delete"`) removes on the
|
|
832
|
+
* next apply.
|
|
833
|
+
*
|
|
834
|
+
* Candidates carry the requested identity verbatim, because that identity is
|
|
835
|
+
* what selected them: the roots enumerated here are this project's own, and
|
|
836
|
+
* their estates are what `terraform.roots` declares for this environment.
|
|
837
|
+
* The estate that actually answered rides `name`, so a reviewer of the plan
|
|
838
|
+
* can see which one it was.
|
|
839
|
+
*
|
|
840
|
+
* `live-ls`'s own `gaps[]` become holes rather than silence, per #1089: a
|
|
841
|
+
* declared instance the tag index can never serve is unknown, not clean.
|
|
842
|
+
*/
|
|
843
|
+
export async function teardownOwned(
|
|
844
|
+
options: { environment: string; marker: OwnershipMarker } & LiveEstateOptions,
|
|
845
|
+
deps: TerraformReadDeps = REAL_DEPS,
|
|
846
|
+
): Promise<TeardownEnumeration> {
|
|
847
|
+
const candidates: TeardownCandidate[] = [];
|
|
848
|
+
const holes: TeardownHole[] = [];
|
|
849
|
+
|
|
850
|
+
for (const { root, estate } of await liveRoots(options)) {
|
|
851
|
+
let listing: LiveLsListing;
|
|
852
|
+
try {
|
|
853
|
+
const listed = await deps.liveLs({
|
|
854
|
+
root,
|
|
855
|
+
consistent: true,
|
|
856
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
857
|
+
});
|
|
858
|
+
listing = readLiveLs(listed.json);
|
|
859
|
+
} catch (err) {
|
|
860
|
+
// Nothing was read, so nothing is known about this root's estate. A
|
|
861
|
+
// hole, never an empty candidate list, which would read as "clean".
|
|
862
|
+
holes.push({
|
|
863
|
+
name: `terraform.roots.${root}`,
|
|
864
|
+
reason: "read-failed",
|
|
865
|
+
detail: `choudoufu live-ls (estate "${estate}"): ${err instanceof Error ? err.message : String(err)}`,
|
|
866
|
+
});
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
for (const item of listing.items) {
|
|
871
|
+
if (item.declared) continue;
|
|
872
|
+
candidates.push({
|
|
873
|
+
name: entityKeyFor(root, item.address ?? item.id),
|
|
874
|
+
type: RESOURCE_TYPE,
|
|
875
|
+
physicalId: item.id,
|
|
876
|
+
marker: options.marker,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
for (const gap of listing.gaps) {
|
|
881
|
+
holes.push({
|
|
882
|
+
name: entityKeyFor(root, gap.address),
|
|
883
|
+
type: RESOURCE_TYPE,
|
|
884
|
+
reason: gapReason(),
|
|
885
|
+
detail: `${gap.rung}: ${gap.detail}`,
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return { candidates, ...(holes.length > 0 ? { holes } : {}) };
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/** The live roots an estate-wide read runs over: the caller's list, or every one declared. */
|
|
894
|
+
async function liveRoots(options: LiveEstateOptions): Promise<Array<{ root: string; estate: string }>> {
|
|
895
|
+
const declared = await liveRootNames(options.cwd);
|
|
896
|
+
if (!options.roots) return declared;
|
|
897
|
+
const wanted = new Set(options.roots);
|
|
898
|
+
return declared.filter((entry) => wanted.has(entry.root));
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/* ─────────────────────────────── dispatch ───────────────────────────────── */
|
|
902
|
+
|
|
903
|
+
/** The activities this reader drives. Injectable so tests never run a binary. */
|
|
169
904
|
export interface TerraformReadDeps {
|
|
170
905
|
init: typeof terraformInit;
|
|
171
906
|
show: typeof terraformShow;
|
|
907
|
+
livePlan: typeof choudoufuLivePlan;
|
|
908
|
+
liveLs: typeof choudoufuLiveLs;
|
|
172
909
|
}
|
|
173
910
|
|
|
174
|
-
const REAL_DEPS: TerraformReadDeps = {
|
|
911
|
+
const REAL_DEPS: TerraformReadDeps = {
|
|
912
|
+
init: terraformInit,
|
|
913
|
+
show: terraformShow,
|
|
914
|
+
livePlan: choudoufuLivePlan,
|
|
915
|
+
liveLs: choudoufuLiveLs,
|
|
916
|
+
};
|
|
175
917
|
|
|
176
|
-
/** A declared entity, plus the root and
|
|
918
|
+
/** A declared entity, plus the root, address and mode `buildRoots()` recorded on it. */
|
|
177
919
|
interface TerraformDeclared extends DeclaredEntity {
|
|
178
920
|
root: string;
|
|
179
921
|
address: string;
|
|
922
|
+
/** `"live"` when the root runs under choudoufu with a declared estate (#2103). */
|
|
923
|
+
mode?: string;
|
|
180
924
|
}
|
|
181
925
|
|
|
182
926
|
/**
|
|
183
|
-
* Split a `<root>/<address>` entity key
|
|
184
|
-
*
|
|
185
|
-
* the
|
|
927
|
+
* Split a `<root>/<address>` entity key, or a descended child module's
|
|
928
|
+
* `<root>/module.<name>/<address>` (#2112). Only a fallback: `buildRoots()`
|
|
929
|
+
* records the root, the address and the call chain on `props`, and a
|
|
930
|
+
* duplicated address is keyed `…~2` there, so the props are the reliable
|
|
931
|
+
* source.
|
|
186
932
|
*/
|
|
187
|
-
function fromEntityName(name: string): { root: string; address: string } {
|
|
933
|
+
function fromEntityName(name: string): { root: string; address: string; callers: string[] } {
|
|
188
934
|
const slash = name.indexOf("/");
|
|
189
|
-
if (slash === -1) return { root: "", address: name };
|
|
190
|
-
|
|
935
|
+
if (slash === -1) return { root: "", address: name, callers: [] };
|
|
936
|
+
const last = name.lastIndexOf("/");
|
|
937
|
+
return {
|
|
938
|
+
root: name.slice(0, slash),
|
|
939
|
+
address: name.slice(last + 1).replace(/~\d+$/, ""),
|
|
940
|
+
callers: callersOfKey(name),
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* The address the live system knows a block by. For a root-module block that
|
|
946
|
+
* is the block's own address; for one inside a child module it is what
|
|
947
|
+
* `terraform show -json` writes in `child_modules[]` and what choudoufu's
|
|
948
|
+
* markers carry, the call chain and the address joined with dots:
|
|
949
|
+
* `module.cdn.null_resource.edge` (#2112). The entity KEY keeps its slashes
|
|
950
|
+
* (`app/module.cdn/null_resource.edge`), so what a build declares and what a
|
|
951
|
+
* read observes still line up one to one.
|
|
952
|
+
*/
|
|
953
|
+
export function qualifiedAddress(address: string, callers: readonly string[]): string {
|
|
954
|
+
return callers.length === 0 ? address : `${callers.join(".")}.${address}`;
|
|
191
955
|
}
|
|
192
956
|
|
|
193
957
|
function declaredOf(name: string, entity: { entityType: string; props: Record<string, unknown> } | undefined): TerraformDeclared {
|
|
194
958
|
const props = entity?.props ?? {};
|
|
195
959
|
const fallback = fromEntityName(name);
|
|
960
|
+
const callers = Array.isArray(props.callers)
|
|
961
|
+
? (props.callers as unknown[]).filter((c): c is string => typeof c === "string")
|
|
962
|
+
: fallback.callers;
|
|
196
963
|
return {
|
|
197
964
|
name,
|
|
198
965
|
type: entity?.entityType ?? "",
|
|
199
966
|
props,
|
|
200
967
|
root: asString(props.root) ?? fallback.root,
|
|
201
|
-
address: asString(props.address) ?? fallback.address,
|
|
968
|
+
address: qualifiedAddress(asString(props.address) ?? fallback.address, callers),
|
|
969
|
+
...(asString(props.mode) ? { mode: asString(props.mode) } : {}),
|
|
202
970
|
};
|
|
203
971
|
}
|
|
204
972
|
|
|
@@ -322,11 +1090,26 @@ export async function describeResources(
|
|
|
322
1090
|
}
|
|
323
1091
|
|
|
324
1092
|
const parts = [];
|
|
1093
|
+
const notes: string[] = [];
|
|
325
1094
|
for (const [root, declared] of byRoot) {
|
|
326
|
-
|
|
1095
|
+
// One fact decides the whole read, and the parse already recorded it on
|
|
1096
|
+
// every entity of the root (#2103). A root is live when its binary is
|
|
1097
|
+
// choudoufu AND it declares an estate, so a single entity carrying
|
|
1098
|
+
// `mode: "live"` settles it for the root.
|
|
1099
|
+
const live = declared.some((entity) => entity.mode === "live");
|
|
1100
|
+
if (live) {
|
|
1101
|
+
notes.push(
|
|
1102
|
+
`terraform.roots.${root} is a live root: ownership came from choudoufu's tofu-estate/tofu-address markers via \`live-plan -json\`, not from a state file`,
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
const read = live
|
|
1106
|
+
? await observeEntities(declared, liveAdapter(root, options.cwd, deps))
|
|
1107
|
+
: await observeEntities(declared, adapter(root, options.cwd, deps));
|
|
1108
|
+
parts.push(normalizeObservation(read));
|
|
327
1109
|
}
|
|
328
1110
|
|
|
329
1111
|
const merged = mergeObservations(parts);
|
|
1112
|
+
merged.notes.push(...notes);
|
|
330
1113
|
const resources = { ...merged.resources };
|
|
331
1114
|
const unobserved = { ...merged.unobserved };
|
|
332
1115
|
|
|
@@ -347,7 +1130,7 @@ export async function describeResources(
|
|
|
347
1130
|
unobserved[name] = {
|
|
348
1131
|
type: meta.type,
|
|
349
1132
|
reason: "filtered",
|
|
350
|
-
detail: "the root's state
|
|
1133
|
+
detail: `this address read \`${meta.ownership ?? "unknown"}\` on the root's ownership channel (its state file, or its live markers) and --owned was requested`,
|
|
351
1134
|
...(merged.queried[name] ? { queried: merged.queried[name] } : {}),
|
|
352
1135
|
};
|
|
353
1136
|
}
|