@intentius/chant-lexicon-terraform 0.57.0 → 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/composites/terraform-adopt-op.d.ts +131 -0
- package/dist/composites/terraform-adopt-op.d.ts.map +1 -0
- package/dist/composites/terraform-apply-op.d.ts +70 -17
- package/dist/composites/terraform-apply-op.d.ts.map +1 -1
- package/dist/composites/terraform-watch-op.d.ts +104 -37
- package/dist/composites/terraform-watch-op.d.ts.map +1 -1
- package/dist/config.d.ts +49 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/describe-resources.d.ts +256 -16
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/hcl/descend.d.ts +98 -0
- package/dist/hcl/descend.d.ts.map +1 -0
- package/dist/hcl/marker.d.ts +130 -0
- package/dist/hcl/marker.d.ts.map +1 -0
- package/dist/hcl/parse.d.ts +156 -9
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/references.d.ts +76 -0
- package/dist/hcl/references.d.ts.map +1 -0
- package/dist/hcl/roots.d.ts +22 -3
- package/dist/hcl/roots.d.ts.map +1 -1
- package/dist/hcl/suppressions.d.ts +54 -0
- package/dist/hcl/suppressions.d.ts.map +1 -0
- package/dist/hcl/value.d.ts +77 -0
- package/dist/hcl/value.d.ts.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +34 -5
- package/dist/lint/audit-catalog.d.ts +3 -2
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +95 -0
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/blocks.d.ts +53 -0
- package/dist/lint/post-synth/blocks.d.ts.map +1 -0
- package/dist/lint/post-synth/fixtures/load.d.ts +37 -0
- package/dist/lint/post-synth/fixtures/load.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/module-source.d.ts +38 -0
- package/dist/lint/post-synth/module-source.d.ts.map +1 -0
- package/dist/lint/post-synth/scope.d.ts +65 -0
- package/dist/lint/post-synth/scope.d.ts.map +1 -0
- package/dist/lint/post-synth/tf001.d.ts +58 -6
- package/dist/lint/post-synth/tf001.d.ts.map +1 -1
- package/dist/lint/post-synth/tf002.d.ts +32 -0
- package/dist/lint/post-synth/tf002.d.ts.map +1 -0
- package/dist/lint/post-synth/tf003.d.ts +22 -0
- package/dist/lint/post-synth/tf003.d.ts.map +1 -0
- package/dist/lint/post-synth/tf004.d.ts +22 -0
- package/dist/lint/post-synth/tf004.d.ts.map +1 -0
- package/dist/lint/post-synth/tf005.d.ts +28 -0
- package/dist/lint/post-synth/tf005.d.ts.map +1 -0
- package/dist/lint/post-synth/tf006.d.ts +21 -0
- package/dist/lint/post-synth/tf006.d.ts.map +1 -0
- package/dist/lint/post-synth/tf007.d.ts +21 -0
- package/dist/lint/post-synth/tf007.d.ts.map +1 -0
- package/dist/lint/post-synth/tf008.d.ts +22 -0
- package/dist/lint/post-synth/tf008.d.ts.map +1 -0
- package/dist/lint/post-synth/tf009.d.ts +24 -0
- package/dist/lint/post-synth/tf009.d.ts.map +1 -0
- package/dist/lint/post-synth/tf010.d.ts +19 -0
- package/dist/lint/post-synth/tf010.d.ts.map +1 -0
- package/dist/lint/post-synth/tf011.d.ts +18 -0
- package/dist/lint/post-synth/tf011.d.ts.map +1 -0
- package/dist/lint/post-synth/tf012.d.ts +17 -0
- package/dist/lint/post-synth/tf012.d.ts.map +1 -0
- package/dist/lint/post-synth/tf013.d.ts +21 -0
- package/dist/lint/post-synth/tf013.d.ts.map +1 -0
- package/dist/lint/post-synth/tf014.d.ts +25 -0
- package/dist/lint/post-synth/tf014.d.ts.map +1 -0
- package/dist/lint/post-synth/tf015.d.ts +30 -0
- package/dist/lint/post-synth/tf015.d.ts.map +1 -0
- package/dist/lint/post-synth/tf016.d.ts +30 -0
- package/dist/lint/post-synth/tf016.d.ts.map +1 -0
- package/dist/lint/post-synth/tf017.d.ts +19 -0
- package/dist/lint/post-synth/tf017.d.ts.map +1 -0
- package/dist/lint/post-synth/tf018.d.ts +20 -0
- package/dist/lint/post-synth/tf018.d.ts.map +1 -0
- package/dist/lint/post-synth/tf019.d.ts +25 -0
- package/dist/lint/post-synth/tf019.d.ts.map +1 -0
- package/dist/lint/post-synth/tf020.d.ts +38 -0
- package/dist/lint/post-synth/tf020.d.ts.map +1 -0
- package/dist/lint/post-synth/tf021.d.ts +41 -0
- package/dist/lint/post-synth/tf021.d.ts.map +1 -0
- package/dist/lint/post-synth/tf022.d.ts +24 -0
- package/dist/lint/post-synth/tf022.d.ts.map +1 -0
- package/dist/lint/post-synth/tf024.d.ts +24 -0
- package/dist/lint/post-synth/tf024.d.ts.map +1 -0
- package/dist/lint/post-synth/tf025.d.ts +29 -0
- package/dist/lint/post-synth/tf025.d.ts.map +1 -0
- package/dist/lint/post-synth/tf026.d.ts +28 -0
- package/dist/lint/post-synth/tf026.d.ts.map +1 -0
- package/dist/lint/post-synth/tf027.d.ts +30 -0
- package/dist/lint/post-synth/tf027.d.ts.map +1 -0
- package/dist/lint/post-synth/tf028.d.ts +33 -0
- package/dist/lint/post-synth/tf028.d.ts.map +1 -0
- package/dist/lint/rules/fixtures/load.d.ts +13 -0
- package/dist/lint/rules/fixtures/load.d.ts.map +1 -0
- package/dist/lint/rules/plan-before-apply.d.ts +11 -0
- package/dist/lint/rules/plan-before-apply.d.ts.map +1 -1
- package/dist/lint/secret-shape.d.ts +68 -0
- package/dist/lint/secret-shape.d.ts.map +1 -0
- package/dist/live-ownership.d.ts +55 -0
- package/dist/live-ownership.d.ts.map +1 -0
- package/dist/lsp/option-keys.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +26 -0
- package/dist/okf/rules/TF002.md +11 -0
- package/dist/okf/rules/TF003.md +11 -0
- package/dist/okf/rules/TF004.md +11 -0
- package/dist/okf/rules/TF005.md +11 -0
- package/dist/okf/rules/TF006.md +11 -0
- package/dist/okf/rules/TF007.md +11 -0
- package/dist/okf/rules/TF008.md +11 -0
- package/dist/okf/rules/TF009.md +11 -0
- package/dist/okf/rules/TF010.md +11 -0
- package/dist/okf/rules/TF011.md +11 -0
- package/dist/okf/rules/TF012.md +11 -0
- package/dist/okf/rules/TF013.md +11 -0
- package/dist/okf/rules/TF014.md +11 -0
- package/dist/okf/rules/TF015.md +11 -0
- package/dist/okf/rules/TF016.md +11 -0
- package/dist/okf/rules/TF017.md +11 -0
- package/dist/okf/rules/TF018.md +11 -0
- package/dist/okf/rules/TF019.md +11 -0
- package/dist/okf/rules/TF020.md +11 -0
- package/dist/okf/rules/TF021.md +11 -0
- package/dist/okf/rules/TF022.md +11 -0
- package/dist/okf/rules/TF024.md +11 -0
- package/dist/okf/rules/TF025.md +11 -0
- package/dist/okf/rules/TF026.md +11 -0
- package/dist/okf/rules/TF027.md +11 -0
- package/dist/okf/rules/TF028.md +11 -0
- package/dist/op/activities/index.d.ts +13 -6
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/live-detect.d.ts +51 -0
- package/dist/op/activities/live-detect.d.ts.map +1 -0
- package/dist/op/activities/terraform.d.ts +466 -17
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +148 -0
- package/dist/op/activity-contracts.d.ts.map +1 -0
- package/dist/op/adoption.d.ts +118 -0
- package/dist/op/adoption.d.ts.map +1 -0
- package/dist/op/builders.d.ts +48 -6
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/resolve-root-mode.d.ts +59 -0
- package/dist/op/resolve-root-mode.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/blocks.ts +109 -0
- package/dist/rules/module-source.ts +121 -0
- package/dist/rules/plan-before-apply.ts +11 -0
- package/dist/rules/scope.ts +107 -0
- package/dist/rules/tf001.ts +98 -18
- package/dist/rules/tf002.ts +151 -0
- package/dist/rules/tf003.ts +71 -0
- package/dist/rules/tf004.ts +63 -0
- package/dist/rules/tf005.ts +76 -0
- package/dist/rules/tf006.ts +53 -0
- package/dist/rules/tf007.ts +78 -0
- package/dist/rules/tf008.ts +75 -0
- package/dist/rules/tf009.ts +59 -0
- package/dist/rules/tf010.ts +51 -0
- package/dist/rules/tf011.ts +49 -0
- package/dist/rules/tf012.ts +47 -0
- package/dist/rules/tf013.ts +63 -0
- package/dist/rules/tf014.ts +72 -0
- package/dist/rules/tf015.ts +72 -0
- package/dist/rules/tf016.ts +75 -0
- package/dist/rules/tf017.ts +50 -0
- package/dist/rules/tf018.ts +72 -0
- package/dist/rules/tf019.ts +72 -0
- package/dist/rules/tf020.ts +123 -0
- package/dist/rules/tf021.ts +126 -0
- package/dist/rules/tf022.ts +61 -0
- package/dist/rules/tf024.ts +70 -0
- package/dist/rules/tf025.ts +101 -0
- package/dist/rules/tf026.ts +84 -0
- package/dist/rules/tf027.ts +74 -0
- package/dist/rules/tf028.ts +96 -0
- package/dist/skills/chant-terraform.md +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/package.json +7 -2
- package/src/__fixtures__/ACCEPTANCE.md +181 -0
- package/src/__fixtures__/live/main.tf +28 -0
- package/src/__fixtures__/live-adopt/estate.chdf.hcl +6 -0
- package/src/__fixtures__/live-adopt/main.tf +21 -0
- package/src/__fixtures__/live-estate/README.md +58 -0
- package/src/__fixtures__/live-estate/compute.tf +10 -0
- package/src/__fixtures__/live-estate/estate.chdf.hcl +1 -0
- package/src/__fixtures__/live-estate/logs.tf +8 -0
- package/src/__fixtures__/live-estate/network.tf +24 -0
- package/src/__fixtures__/live-estate/rule.tf +11 -0
- package/src/__fixtures__/live-estate/unowned.tf +21 -0
- package/src/__fixtures__/live-estate/versions.tf +31 -0
- package/src/__fixtures__/live-ls.json +98 -0
- package/src/__fixtures__/live-plan.json +285 -0
- package/src/__fixtures__/live-sidecar/estate.chdf.hcl +3 -0
- package/src/__fixtures__/live-sidecar/main.tf +24 -0
- package/src/__fixtures__/module-tree/README.md +16 -0
- package/src/__fixtures__/module-tree/cycle/main.tf +3 -0
- package/src/__fixtures__/module-tree/cycle/modules/a/main.tf +9 -0
- package/src/__fixtures__/module-tree/cycle/modules/b/main.tf +9 -0
- package/src/__fixtures__/module-tree/missing/main.tf +9 -0
- package/src/__fixtures__/module-tree/outside/root/main.tf +9 -0
- package/src/__fixtures__/module-tree/root/main.tf +28 -0
- package/src/__fixtures__/module-tree/root/modules/cdn/main.tf +15 -0
- package/src/__fixtures__/module-tree/root/modules/cdn/modules/bucket/main.tf +11 -0
- package/src/__fixtures__/module-tree/shared/main.tf +5 -0
- package/src/__fixtures__/references/main.tf +56 -0
- package/src/__fixtures__/remote-backend/main.tf +34 -0
- package/src/__fixtures__/with-backend/main.tf +7 -0
- package/src/codegen/lint-rules-page.test.ts +45 -0
- package/src/composites/terraform-adopt-op.acceptance.test.ts +193 -0
- package/src/composites/terraform-adopt-op.test.ts +175 -0
- package/src/composites/terraform-adopt-op.ts +202 -0
- package/src/composites/terraform-apply-op.acceptance.test.ts +188 -5
- package/src/composites/terraform-apply-op.test.ts +179 -19
- package/src/composites/terraform-apply-op.ts +123 -30
- package/src/composites/terraform-watch-op.test.ts +169 -29
- package/src/composites/terraform-watch-op.ts +183 -75
- package/src/config.ts +32 -3
- package/src/describe-resources.live.test.ts +598 -0
- package/src/describe-resources.test.ts +53 -0
- package/src/describe-resources.ts +811 -28
- package/src/hcl/descend.test.ts +162 -0
- package/src/hcl/descend.ts +273 -0
- package/src/hcl/marker.test.ts +147 -0
- package/src/hcl/marker.ts +235 -0
- package/src/hcl/parse.test.ts +69 -0
- package/src/hcl/parse.ts +269 -27
- package/src/hcl/references.test.ts +116 -0
- package/src/hcl/references.ts +172 -0
- package/src/hcl/roots.ts +59 -5
- package/src/hcl/suppressions.test.ts +147 -0
- package/src/hcl/suppressions.ts +204 -0
- package/src/hcl/value.test.ts +73 -0
- package/src/hcl/value.ts +106 -0
- package/src/index.ts +31 -1
- package/src/lint/audit-catalog.ts +288 -3
- package/src/lint/audit-lineage.ts +384 -1
- package/src/lint/audit.test.ts +210 -3
- package/src/lint/post-synth/blocks.ts +109 -0
- package/src/lint/post-synth/fixtures/TF001/negative-cloud.tf +28 -0
- package/src/lint/post-synth/fixtures/TF001/negative.tf +30 -0
- package/src/lint/post-synth/fixtures/TF001/positive-local.tf +31 -0
- package/src/lint/post-synth/fixtures/TF001/positive.tf +24 -0
- package/src/lint/post-synth/fixtures/TF002/negative.tf +17 -0
- package/src/lint/post-synth/fixtures/TF002/positive-legacy.tf +10 -0
- package/src/lint/post-synth/fixtures/TF002/positive-no-version.tf +12 -0
- package/src/lint/post-synth/fixtures/TF002/positive.tf +7 -0
- package/src/lint/post-synth/fixtures/TF003/negative.tf +10 -0
- package/src/lint/post-synth/fixtures/TF003/positive.tf +8 -0
- package/src/lint/post-synth/fixtures/TF004/negative-git.tf +3 -0
- package/src/lint/post-synth/fixtures/TF004/negative-local.tf +3 -0
- package/src/lint/post-synth/fixtures/TF004/negative.tf +4 -0
- package/src/lint/post-synth/fixtures/TF004/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/negative-local.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/negative-registry.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/negative-sha.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/negative.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/positive-branch.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/positive-non-semver.tf +3 -0
- package/src/lint/post-synth/fixtures/TF005/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF006/negative.tf +5 -0
- package/src/lint/post-synth/fixtures/TF006/positive-empty-default.tf +11 -0
- package/src/lint/post-synth/fixtures/TF006/positive.tf +6 -0
- package/src/lint/post-synth/fixtures/TF007/negative.tf +10 -0
- package/src/lint/post-synth/fixtures/TF007/positive.tf +9 -0
- package/src/lint/post-synth/fixtures/TF008/negative-variable.tf +10 -0
- package/src/lint/post-synth/fixtures/TF008/negative.tf +8 -0
- package/src/lint/post-synth/fixtures/TF008/positive-other-provider.tf +10 -0
- package/src/lint/post-synth/fixtures/TF008/positive.tf +5 -0
- package/src/lint/post-synth/fixtures/TF009/negative.tf +10 -0
- package/src/lint/post-synth/fixtures/TF009/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF010/negative.tf +5 -0
- package/src/lint/post-synth/fixtures/TF010/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF011/negative.tf +5 -0
- package/src/lint/post-synth/fixtures/TF011/positive-blank.tf +5 -0
- package/src/lint/post-synth/fixtures/TF011/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF012/negative.tf +4 -0
- package/src/lint/post-synth/fixtures/TF012/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF013/negative.tf +8 -0
- package/src/lint/post-synth/fixtures/TF013/positive-data.tf +8 -0
- package/src/lint/post-synth/fixtures/TF013/positive.tf +8 -0
- package/src/lint/post-synth/fixtures/TF014/negative/main.tf +26 -0
- package/src/lint/post-synth/fixtures/TF014/negative/modules/cdn/main.tf +19 -0
- package/src/lint/post-synth/fixtures/TF014/positive/main.tf +16 -0
- package/src/lint/post-synth/fixtures/TF014/positive/modules/cdn/main.tf +15 -0
- package/src/lint/post-synth/fixtures/TF015/negative/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF015/negative/modules/cdn/main.tf +16 -0
- package/src/lint/post-synth/fixtures/TF015/positive/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF015/positive/modules/cdn/main.tf +14 -0
- package/src/lint/post-synth/fixtures/TF016/negative-template.tf +7 -0
- package/src/lint/post-synth/fixtures/TF016/negative.tf +16 -0
- package/src/lint/post-synth/fixtures/TF016/positive-shared-file.tf +12 -0
- package/src/lint/post-synth/fixtures/TF016/positive.tf +13 -0
- package/src/lint/post-synth/fixtures/TF017/negative.tf +4 -0
- package/src/lint/post-synth/fixtures/TF017/positive.tf +5 -0
- package/src/lint/post-synth/fixtures/TF018/negative.tf +9 -0
- package/src/lint/post-synth/fixtures/TF018/positive-data.tf +9 -0
- package/src/lint/post-synth/fixtures/TF018/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF019/negative.tf +14 -0
- package/src/lint/post-synth/fixtures/TF019/positive-output.tf +5 -0
- package/src/lint/post-synth/fixtures/TF019/positive.tf +16 -0
- package/src/lint/post-synth/fixtures/TF020/negative.tf +43 -0
- package/src/lint/post-synth/fixtures/TF020/positive.tf +32 -0
- package/src/lint/post-synth/fixtures/TF020/scoped/main.tf +21 -0
- package/src/lint/post-synth/fixtures/TF020/scoped/modules/cdn/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF021/negative.tf +31 -0
- package/src/lint/post-synth/fixtures/TF021/positive-length.tf +8 -0
- package/src/lint/post-synth/fixtures/TF021/positive.tf +8 -0
- package/src/lint/post-synth/fixtures/TF022/negative-locator.tf +7 -0
- package/src/lint/post-synth/fixtures/TF022/negative.tf +10 -0
- package/src/lint/post-synth/fixtures/TF022/positive-nested.tf +10 -0
- package/src/lint/post-synth/fixtures/TF022/positive.tf +6 -0
- package/src/lint/post-synth/fixtures/TF023/negative/main.tf +15 -0
- package/src/lint/post-synth/fixtures/TF023/positive/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF023/positive/state.json +24 -0
- package/src/lint/post-synth/fixtures/TF024/live-clean/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF024/live-with-backend/main.tf +14 -0
- package/src/lint/post-synth/fixtures/TF024/live-with-cloud/main.tf +16 -0
- package/src/lint/post-synth/fixtures/TF025/live-clean/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF025/live-with-workspace-ref/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF026/live-default/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF026/live-explicit-delete/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF026/live-keep/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF027/live-default/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF027/live-untagged-delete/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF027/live-untagged-delete-scoped/main.tf +17 -0
- package/src/lint/post-synth/fixtures/TF027/live-untagged-report/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF028/live/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF028/stock/main.tf +10 -0
- package/src/lint/post-synth/fixtures/load.ts +108 -0
- package/src/lint/post-synth/index.ts +52 -0
- package/src/lint/post-synth/live-refusals-config-format.test.ts +143 -0
- package/src/lint/post-synth/module-rules.test.ts +153 -0
- package/src/lint/post-synth/module-source.test.ts +75 -0
- package/src/lint/post-synth/module-source.ts +121 -0
- package/src/lint/post-synth/post-synth.test.ts +73 -16
- package/src/lint/post-synth/scope.ts +107 -0
- package/src/lint/post-synth/single-block.test.ts +263 -0
- package/src/lint/post-synth/suppression.test.ts +110 -0
- package/src/lint/post-synth/tf001.ts +98 -18
- package/src/lint/post-synth/tf002.test.ts +93 -0
- package/src/lint/post-synth/tf002.ts +151 -0
- package/src/lint/post-synth/tf003.test.ts +75 -0
- package/src/lint/post-synth/tf003.ts +71 -0
- package/src/lint/post-synth/tf004.test.ts +47 -0
- package/src/lint/post-synth/tf004.ts +63 -0
- package/src/lint/post-synth/tf005.test.ts +57 -0
- package/src/lint/post-synth/tf005.ts +76 -0
- package/src/lint/post-synth/tf006.ts +53 -0
- package/src/lint/post-synth/tf007.ts +78 -0
- package/src/lint/post-synth/tf008.ts +75 -0
- package/src/lint/post-synth/tf009.ts +59 -0
- package/src/lint/post-synth/tf010.ts +51 -0
- package/src/lint/post-synth/tf011.ts +49 -0
- package/src/lint/post-synth/tf012.ts +47 -0
- package/src/lint/post-synth/tf013.ts +63 -0
- package/src/lint/post-synth/tf014.ts +72 -0
- package/src/lint/post-synth/tf015.ts +72 -0
- package/src/lint/post-synth/tf016.ts +75 -0
- package/src/lint/post-synth/tf017.ts +50 -0
- package/src/lint/post-synth/tf018.ts +72 -0
- package/src/lint/post-synth/tf019.ts +72 -0
- package/src/lint/post-synth/tf020.ts +123 -0
- package/src/lint/post-synth/tf021.ts +126 -0
- package/src/lint/post-synth/tf022.ts +61 -0
- package/src/lint/post-synth/tf024.test.ts +42 -0
- package/src/lint/post-synth/tf024.ts +70 -0
- package/src/lint/post-synth/tf025.test.ts +46 -0
- package/src/lint/post-synth/tf025.ts +101 -0
- package/src/lint/post-synth/tf026.test.ts +56 -0
- package/src/lint/post-synth/tf026.ts +84 -0
- package/src/lint/post-synth/tf027.test.ts +62 -0
- package/src/lint/post-synth/tf027.ts +74 -0
- package/src/lint/post-synth/tf028.test.ts +92 -0
- package/src/lint/post-synth/tf028.ts +96 -0
- package/src/lint/presets.test.ts +72 -0
- package/src/lint/rules/fixtures/TF101/negative-op.ts +2 -0
- package/src/lint/rules/fixtures/TF101/positive-op.ts +2 -0
- package/src/lint/rules/fixtures/load.ts +53 -0
- package/src/lint/rules/plan-before-apply.test.ts +112 -7
- package/src/lint/rules/plan-before-apply.ts +11 -0
- package/src/lint/secret-shape.test.ts +150 -0
- package/src/lint/secret-shape.ts +159 -0
- package/src/live-ownership.ts +60 -0
- package/src/lsp/completions.test.ts +3 -2
- package/src/lsp/option-keys.ts +12 -6
- package/src/op/activities/choudoufu.acceptance.test.ts +127 -0
- package/src/op/activities/choudoufu.test.ts +725 -0
- package/src/op/activities/index.ts +40 -4
- package/src/op/activities/live-detect.test.ts +143 -0
- package/src/op/activities/live-detect.ts +162 -0
- package/src/op/activities/registry.test.ts +11 -3
- package/src/op/activities/terraform.test.ts +7 -3
- package/src/op/activities/terraform.ts +861 -69
- package/src/op/activity-contracts.test.ts +147 -0
- package/src/op/activity-contracts.ts +178 -0
- package/src/op/adoption.test.ts +170 -0
- package/src/op/adoption.ts +241 -0
- package/src/op/builders.test.ts +60 -5
- package/src/op/builders.ts +95 -5
- package/src/op/resolve-root-mode.test.ts +85 -0
- package/src/op/resolve-root-mode.ts +86 -0
- package/src/plugin.test.ts +106 -0
- package/src/plugin.ts +186 -29
- package/src/skills/chant-terraform.md +1 -1
- package/src/validate.ts +10 -1
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* only the `-no-color` plan text can reach an issue or PR body.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { afterEach, describe, test, expect } from "vitest";
|
|
8
11
|
import {
|
|
9
12
|
isStepOutputRef,
|
|
10
13
|
collectStepOutputRefs,
|
|
@@ -45,7 +48,7 @@ describe("TerraformWatchOp phases (#2087)", () => {
|
|
|
45
48
|
}
|
|
46
49
|
});
|
|
47
50
|
|
|
48
|
-
test("the Plan step reports drift as a
|
|
51
|
+
test("the Plan step reports drift as a run outcome", () => {
|
|
49
52
|
const plan = props({ name: "app-watch", root: "app" }).phases[1].steps[0] as ActivityStep;
|
|
50
53
|
expect(plan.fn).toBe("terraformPlan");
|
|
51
54
|
expect(plan.id).toBe("plan");
|
|
@@ -71,7 +74,7 @@ describe("TerraformWatchOp phases (#2087)", () => {
|
|
|
71
74
|
});
|
|
72
75
|
|
|
73
76
|
describe("TerraformWatchOp finding modes (#2087)", () => {
|
|
74
|
-
const findingStep = (mode: "issue" | "pull-request"): ActivityStep => {
|
|
77
|
+
const findingStep = (mode: "issue" | "comment" | "pull-request"): ActivityStep => {
|
|
75
78
|
const op = props({ name: "app-watch", root: "app", findingMode: mode });
|
|
76
79
|
expect(phaseNames(op)).toEqual(["Init", "Plan", "Report"]);
|
|
77
80
|
return op.phases[2].steps[0] as ActivityStep;
|
|
@@ -92,6 +95,14 @@ describe("TerraformWatchOp finding modes (#2087)", () => {
|
|
|
92
95
|
expect(step.outcomeAttribute).toEqual({ name: "PR", from: "prUrl" });
|
|
93
96
|
});
|
|
94
97
|
|
|
98
|
+
test("comment mode posts on the triggering pull request and surfaces the comment URL (#2231)", () => {
|
|
99
|
+
const step = findingStep("comment");
|
|
100
|
+
expect(step.fn).toBe("reconcilePr");
|
|
101
|
+
expect(step.args?.mode).toBe("comment");
|
|
102
|
+
expect(step.args?.env).toBe("app");
|
|
103
|
+
expect(step.outcomeAttribute).toEqual({ name: "Comment", from: "commentUrl" });
|
|
104
|
+
});
|
|
105
|
+
|
|
95
106
|
test("the finding step derives no plan of its own", () => {
|
|
96
107
|
// `entries: []` is what stops reconcilePr shelling to `chant lifecycle
|
|
97
108
|
// plan --json`: the finding is already written by the time it runs.
|
|
@@ -114,7 +125,7 @@ describe("TerraformWatchOp finding modes (#2087)", () => {
|
|
|
114
125
|
});
|
|
115
126
|
|
|
116
127
|
describe("TerraformWatchOp posts the human plan and never the plan JSON (#2087)", () => {
|
|
117
|
-
for (const mode of ["issue", "pull-request"] as const) {
|
|
128
|
+
for (const mode of ["issue", "comment", "pull-request"] as const) {
|
|
118
129
|
test(`${mode}: the body is a reference to the Plan step's -no-color text`, () => {
|
|
119
130
|
const op = props({ name: "app-watch", root: "app", findingMode: mode });
|
|
120
131
|
const report = op.phases[2].steps[0] as ActivityStep;
|
|
@@ -145,40 +156,169 @@ describe("TerraformWatchOp posts the human plan and never the plan JSON (#2087)"
|
|
|
145
156
|
});
|
|
146
157
|
});
|
|
147
158
|
|
|
148
|
-
describe("TerraformWatchOp
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
159
|
+
describe("TerraformWatchOp on a live root (#2105)", () => {
|
|
160
|
+
const live = (over: Partial<Parameters<typeof TerraformWatchOp>[0]> = {}): OpConfig =>
|
|
161
|
+
props({ name: "estate-watch", root: "estate", live: true, ...over });
|
|
162
|
+
|
|
163
|
+
test("the Plan step is choudoufuLivePlan, and Init is unchanged", () => {
|
|
164
|
+
const op = live();
|
|
165
|
+
expect(phaseNames(op)).toEqual(["Init", "Plan"]);
|
|
166
|
+
expect(steps(op).map((s) => s.fn)).toEqual(["terraformInit", "choudoufuLivePlan"]);
|
|
153
167
|
});
|
|
154
168
|
|
|
155
|
-
test("
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
169
|
+
test("one live read publishes Drift, Unowned and Adoptable", () => {
|
|
170
|
+
const plan = live().phases[1].steps[0] as ActivityStep;
|
|
171
|
+
expect(plan.id).toBe("plan");
|
|
172
|
+
// `drift` off `-detailed-exitcode`, `unowned`/`adoptable` off the same
|
|
173
|
+
// run's #788 document: three answers, one estate-wide sweep.
|
|
174
|
+
expect(plan.outcomeAttribute).toEqual([
|
|
175
|
+
{ name: "Drift", from: "drift" },
|
|
176
|
+
{ name: "Unowned", from: "unowned" },
|
|
177
|
+
{ name: "Adoptable", from: "adoptable" },
|
|
178
|
+
]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("the Op is marked live, and no plan file is named anywhere", () => {
|
|
182
|
+
const op = live();
|
|
183
|
+
expect(op.labels).toMatchObject({ TerraformMode: "live" });
|
|
184
|
+
expect(JSON.stringify(op)).not.toContain("chant.tfplan");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("planFile on a live root is refused at build time, quoting choudoufu", () => {
|
|
188
|
+
expect(() => live({ planFile: "estate.tfplan" })).toThrow(/planFile is refused on a live root/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("estate off a live root is refused: there is no live plan to name one for", () => {
|
|
192
|
+
expect(() => props({ name: "app-watch", root: "app", estate: "prod" })).toThrow(/not on a live root/);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("an explicit estate rides the Plan step", () => {
|
|
196
|
+
const plan = live({ estate: "prod-networking" }).phases[1].steps[0] as ActivityStep;
|
|
197
|
+
expect(plan.args?.estate).toBe("prod-networking");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("a stock root is untouched: still terraformPlan, still one Drift attribute", () => {
|
|
201
|
+
const plan = props({ name: "app-watch", root: "app" }).phases[1].steps[0] as ActivityStep;
|
|
202
|
+
expect(plan.fn).toBe("terraformPlan");
|
|
203
|
+
expect(plan.outcomeAttribute).toEqual({ name: "Drift", from: "changed" });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
for (const mode of ["issue", "comment", "pull-request"] as const) {
|
|
207
|
+
test(`${mode}: the body is the plan text plus the adoption ledger, as one field`, () => {
|
|
208
|
+
const op = live({ findingMode: mode });
|
|
209
|
+
expect(phaseNames(op)).toEqual(["Init", "Plan", "Report"]);
|
|
210
|
+
const report = op.phases[2].steps[0] as ActivityStep;
|
|
211
|
+
expect(report.fn).toBe("reconcilePr");
|
|
212
|
+
expect(isStepOutputRef(report.args?.body)).toBe(true);
|
|
213
|
+
expect(report.args?.body).toMatchObject({ step: "plan", path: "finding" });
|
|
160
214
|
});
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
215
|
+
|
|
216
|
+
test(`${mode}: no reference to the live-plan document reaches the body`, () => {
|
|
217
|
+
// On a live root the JSON is choudoufu's bound/omissions/unowned
|
|
218
|
+
// document: live identities and tag values for every resource the
|
|
219
|
+
// estate touched. Same rule as the stock plan's JSON, same test.
|
|
220
|
+
const op = live({ findingMode: mode });
|
|
221
|
+
const report = op.phases[2].steps[0] as ActivityStep;
|
|
222
|
+
const paths = collectStepOutputRefs(report.args).map((r) => r.path);
|
|
223
|
+
expect(paths).toEqual(["finding"]);
|
|
224
|
+
expect(paths).not.toContain("json");
|
|
225
|
+
expect(JSON.stringify(report.args)).not.toContain("json");
|
|
168
226
|
});
|
|
169
|
-
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
test("the whole live Op references exactly one field of the plan", () => {
|
|
230
|
+
const op = live({ findingMode: "issue" });
|
|
231
|
+
const refs = op.phases.flatMap((p) => p.steps.filter(isActivity)).flatMap((s) => collectStepOutputRefs(s.args));
|
|
232
|
+
expect(refs).toHaveLength(1);
|
|
233
|
+
expect(refs[0]).toMatchObject({ step: "plan", path: "finding" });
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("TerraformWatchOp schedule (#2087, #2120)", () => {
|
|
238
|
+
test("omitting `schedule` leaves the Op with no cadence", () => {
|
|
239
|
+
const { op } = TerraformWatchOp({ name: "app-watch", root: "app" });
|
|
240
|
+
expect((op as unknown as { props: OpConfig }).props).not.toHaveProperty("schedule");
|
|
170
241
|
});
|
|
171
242
|
|
|
172
|
-
test("
|
|
173
|
-
const { op
|
|
243
|
+
test("a cron lands on the Op itself", () => {
|
|
244
|
+
const { op } = TerraformWatchOp({
|
|
174
245
|
name: "app-watch",
|
|
175
246
|
root: "app",
|
|
176
247
|
schedule: "0 6 * * *",
|
|
177
|
-
taskQueue: "infra",
|
|
178
248
|
});
|
|
179
|
-
expect((op as unknown as { props: OpConfig }).props.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
)
|
|
249
|
+
expect((op as unknown as { props: OpConfig }).props.schedule).toEqual({
|
|
250
|
+
cron: "0 6 * * *",
|
|
251
|
+
overlap: "skip",
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The `live` flag against the root it names (#2216). Best-effort: the mode
|
|
258
|
+
* resolves from a `chant.config.json` only, so these projects are written in
|
|
259
|
+
* that format. TF028 (`../lint/post-synth/tf028.ts`) is what reports the same
|
|
260
|
+
* mismatch on a `chant.config.ts` project, where the mode does not resolve
|
|
261
|
+
* here at all.
|
|
262
|
+
*/
|
|
263
|
+
describe("TerraformWatchOp cross-checks live against the root's mode (#2216)", () => {
|
|
264
|
+
const dirs: string[] = [];
|
|
265
|
+
|
|
266
|
+
afterEach(() => {
|
|
267
|
+
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const LIVE_ROOT = [
|
|
271
|
+
"terraform {",
|
|
272
|
+
" live {",
|
|
273
|
+
' estate = "prod-estate"',
|
|
274
|
+
" }",
|
|
275
|
+
"}",
|
|
276
|
+
"",
|
|
277
|
+
].join("\n");
|
|
278
|
+
|
|
279
|
+
/** A project on a `chant.config.json` whose root `estate` is `mainTf`, run on `binary`. */
|
|
280
|
+
function project(binary: string, mainTf: string): string {
|
|
281
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-tf-watch-mode-"));
|
|
282
|
+
dirs.push(dir);
|
|
283
|
+
mkdirSync(join(dir, "estate"), { recursive: true });
|
|
284
|
+
writeFileSync(join(dir, "estate", "main.tf"), mainTf);
|
|
285
|
+
writeFileSync(
|
|
286
|
+
join(dir, "chant.config.json"),
|
|
287
|
+
JSON.stringify({ terraform: { binary, roots: { estate: { dir: "./estate" } } } }),
|
|
288
|
+
);
|
|
289
|
+
return dir;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
test("refuses a live root watched in stock mode, naming the Op and the root", () => {
|
|
293
|
+
const cwd = project("choudoufu", LIVE_ROOT);
|
|
294
|
+
expect(() => TerraformWatchOp({ name: "estate-watch", root: "estate", cwd })).toThrow(
|
|
295
|
+
/TerraformWatchOp "estate-watch": root "estate" runs choudoufu with a declared estate/,
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("accepts the same root with live: true", () => {
|
|
300
|
+
const cwd = project("choudoufu", LIVE_ROOT);
|
|
301
|
+
expect(() => TerraformWatchOp({ name: "estate-watch", root: "estate", live: true, cwd })).not.toThrow();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("refuses live: true on a root that declares no estate", () => {
|
|
305
|
+
const cwd = project("choudoufu", 'resource "null_resource" "first" {}\n');
|
|
306
|
+
expect(() => TerraformWatchOp({ name: "app-watch", root: "estate", live: true, cwd })).toThrow(
|
|
307
|
+
/declares no estate/,
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("refuses live: true when the binary is not choudoufu, whatever the root declares", () => {
|
|
312
|
+
const cwd = project("terraform", LIVE_ROOT);
|
|
313
|
+
expect(() => TerraformWatchOp({ name: "app-watch", root: "estate", live: true, cwd })).toThrow(
|
|
314
|
+
/declares no estate/,
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("says nothing when the mode does not resolve: no config to read", () => {
|
|
319
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-tf-watch-mode-"));
|
|
320
|
+
dirs.push(dir);
|
|
321
|
+
expect(() => TerraformWatchOp({ name: "app-watch", root: "estate", cwd: dir })).not.toThrow();
|
|
322
|
+
expect(() => TerraformWatchOp({ name: "app-watch", root: "estate", live: true, cwd: dir })).not.toThrow();
|
|
183
323
|
});
|
|
184
324
|
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* the plan found. `terraform plan -detailed-exitcode` is the whole mechanism.
|
|
8
8
|
* Exit 0 is "the world matches the configuration", exit 2 is drift, and
|
|
9
9
|
* `terraformPlan` turns that into the boolean `changed`, which rides out of
|
|
10
|
-
* the Op as the `Drift`
|
|
10
|
+
* the Op as the run's `Drift` outcome.
|
|
11
11
|
*
|
|
12
12
|
* Phases: Init, Plan, and (for a finding mode that opens something) Report.
|
|
13
13
|
*
|
|
@@ -23,37 +23,59 @@
|
|
|
23
23
|
* post the JSON, and `terraform-watch-op.test.ts` asserts no reference to it
|
|
24
24
|
* reaches the finding step.
|
|
25
25
|
*
|
|
26
|
+
* The same rule holds on a live root, where the JSON is choudoufu's own
|
|
27
|
+
* bound/omissions/unowned document rather than a plan representation, and
|
|
28
|
+
* carries live identities and tag values for every resource the estate
|
|
29
|
+
* touched. `choudoufuLivePlan` composes the plan text and the adoption ledger
|
|
30
|
+
* into one `finding` string for exactly this reason: the body is one
|
|
31
|
+
* reference to one text field, so there is no arrangement of the args in
|
|
32
|
+
* which the document reaches a body.
|
|
33
|
+
*
|
|
34
|
+
* ## A live root reports three things, not one (#2105)
|
|
35
|
+
*
|
|
36
|
+
* `live: true` swaps the Plan step for `choudoufuLivePlan`. On a choudoufu
|
|
37
|
+
* root ownership is a pair of tags on the resource rather than an entry in a
|
|
38
|
+
* state file, so a plan against the live system can answer three questions
|
|
39
|
+
* where a stock plan answers one: whether the estate drifted (the
|
|
40
|
+
* `-detailed-exitcode` exit, `Drift` as before), how many live resources sit
|
|
41
|
+
* at a declared identity carrying no marker (`Unowned`), and how many of those
|
|
42
|
+
* an exact content match makes claimable (`Adoptable`). All three come off a
|
|
43
|
+
* single live read, published as three run outcomes from the one step.
|
|
44
|
+
*
|
|
45
|
+
* The finding modes then carry the adoption ledger under the plan text: one
|
|
46
|
+
* line per adoptable match with its address, its live identity and the
|
|
47
|
+
* `tofu-estate`/`tofu-address` values that claim it, in the row form
|
|
48
|
+
* `live-plan -adoption-only` prints, followed by any contested address. That
|
|
49
|
+
* is a report, not an action. `TerraformAdoptOp` next door is what writes the
|
|
50
|
+
* markers, and it gates first.
|
|
51
|
+
*
|
|
26
52
|
* ## Findings reuse `reconcilePr`
|
|
27
53
|
*
|
|
28
|
-
* `issue` and `pull-request` call
|
|
29
|
-
*
|
|
30
|
-
*
|
|
54
|
+
* `issue`, `comment` and `pull-request` call core's `reconcilePr` activity,
|
|
55
|
+
* the one place in chant that shells to `gh`, rather than growing a second
|
|
56
|
+
* copy of those calls here.
|
|
31
57
|
* That activity built its own change-set summary and had no way to be handed
|
|
32
58
|
* one, so #2087 added a single field to it (`ReconcilePrArgs.body`), which is
|
|
33
59
|
* what carries the `-no-color` plan through. Note that the pull-request mode
|
|
34
60
|
* of `reconcilePr` regenerates chant TypeScript via `chant import`; a
|
|
35
|
-
* terraform-native regeneration of HCL from live state is #2089, so
|
|
36
|
-
*
|
|
37
|
-
* `"pull-request"` opens a PR whose body is the plan.
|
|
61
|
+
* terraform-native regeneration of HCL from live state is #2089, so
|
|
62
|
+
* `findingMode: "issue"` is the mode with an end-to-end answer and
|
|
63
|
+
* `"pull-request"` opens a PR whose body is the plan. `"comment"` (#2231) is
|
|
64
|
+
* the mode for the plan-on-pull-request shape: the plan is posted onto the
|
|
65
|
+
* pull request that triggered the run and re-edited there on the next push,
|
|
66
|
+
* which is where a reviewer is already looking. It needs that trigger; a run
|
|
67
|
+
* without one fails the Report step by name rather than posting elsewhere. On a live root #2089's
|
|
68
|
+
* question has a different answer rather than a pending one, and it is not
|
|
69
|
+
* regeneration: see `TerraformAdoptOp`.
|
|
38
70
|
*
|
|
39
71
|
* ## The schedule
|
|
40
72
|
*
|
|
41
|
-
* A cron
|
|
42
|
-
* `
|
|
43
|
-
* `
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* project that installs terraform and runs `chant run` on the local executor
|
|
48
|
-
* could not load `@intentius/chant-lexicon-terraform` at all without also
|
|
49
|
-
* installing temporal. The declarable is the same one temporal's
|
|
50
|
-
* `TemporalSchedule` produces (`createResource("Temporal::Schedule",
|
|
51
|
-
* "temporal", {})`, `lexicons/temporal/src/resources.ts`), so temporal's
|
|
52
|
-
* serializer renders it unchanged when a project has both.
|
|
53
|
-
*
|
|
54
|
-
* A project with no Temporal at all runs the same Op on a CI cron instead:
|
|
55
|
-
* `generateOpsPipeline` against the github lexicon turns the Op plus its cron
|
|
56
|
-
* into a workflow. `examples/scheduled-watch/` is that recipe.
|
|
73
|
+
* A cron lands on the Op itself as `schedule` (#2120), the same field
|
|
74
|
+
* `WatchOp` and `ConvergeOp` set. It is runtime-neutral data: `chant
|
|
75
|
+
* operator` reads it as this Op's tick cadence, `generateOpsPipeline` against
|
|
76
|
+
* the github lexicon renders it as a CI cron, and a hosting lexicon hands it
|
|
77
|
+
* to its own scheduler. The one-shot `chant run app-watch` path ignores it.
|
|
78
|
+
* `examples/scheduled-watch/` is the CI recipe.
|
|
57
79
|
*
|
|
58
80
|
* @example
|
|
59
81
|
* ```typescript
|
|
@@ -62,59 +84,100 @@
|
|
|
62
84
|
* // one-shot, local executor: chant run app-watch
|
|
63
85
|
* export const { op } = TerraformWatchOp({ name: "app-watch", root: "app" });
|
|
64
86
|
*
|
|
87
|
+
* // the plan as a sticky comment on the pull request that triggered the run
|
|
88
|
+
* export const { op } = TerraformWatchOp({
|
|
89
|
+
* name: "app-plan",
|
|
90
|
+
* root: "app",
|
|
91
|
+
* findingMode: "comment",
|
|
92
|
+
* });
|
|
93
|
+
*
|
|
65
94
|
* // nightly, opening an issue when the plan is non-empty
|
|
66
|
-
* export const { op
|
|
95
|
+
* export const { op } = TerraformWatchOp({
|
|
67
96
|
* name: "app-watch",
|
|
68
97
|
* root: "app",
|
|
69
98
|
* schedule: "0 6 * * *",
|
|
70
99
|
* findingMode: "issue",
|
|
71
100
|
* });
|
|
101
|
+
*
|
|
102
|
+
* // a choudoufu estate: drift, unowned and adoptable, with the ledger in the issue
|
|
103
|
+
* export const { op } = TerraformWatchOp({
|
|
104
|
+
* name: "estate-watch",
|
|
105
|
+
* root: "estate",
|
|
106
|
+
* live: true,
|
|
107
|
+
* findingMode: "issue",
|
|
108
|
+
* });
|
|
72
109
|
* ```
|
|
73
110
|
*/
|
|
74
111
|
|
|
75
112
|
import { Op, phase, OpResource, type ActivityStep } from "@intentius/chant/op";
|
|
76
|
-
import {
|
|
77
|
-
import
|
|
78
|
-
import {
|
|
79
|
-
|
|
113
|
+
import { resolveRootModeSync } from "../op/resolve-root-mode";
|
|
114
|
+
import { CHOUDOUFU_LIVE_PLAN_OUT_REFUSAL, DEFAULT_PLAN_FILE } from "../op/activities/terraform";
|
|
115
|
+
import {
|
|
116
|
+
terraformInit as initStep,
|
|
117
|
+
terraformPlan as planStep,
|
|
118
|
+
choudoufuLivePlan as livePlanStep,
|
|
119
|
+
} from "../op/builders";
|
|
80
120
|
|
|
81
121
|
/**
|
|
82
122
|
* What to do with a non-empty plan. A subset of core's `OpFindingMode`
|
|
83
123
|
* (`packages/core/src/lexicon.ts`): `merge-request` is GitLab's spelling of
|
|
84
124
|
* `pull-request` and is produced by the CI generator, not chosen here.
|
|
125
|
+
*
|
|
126
|
+
* `comment` (#2231) is the mode a plan-on-pull-request Op wants: the plan
|
|
127
|
+
* lands as one comment on the pull request that triggered the run and the
|
|
128
|
+
* next push edits that same comment. It needs a `pull_request` trigger on the
|
|
129
|
+
* `ScheduledOpSpec` the workflow is generated from, and the github generator
|
|
130
|
+
* refuses it by name on any other trigger.
|
|
85
131
|
*/
|
|
86
|
-
export type TerraformFindingMode = "report" | "issue" | "pull-request";
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* `Temporal::Schedule`, built without importing the temporal lexicon. See the
|
|
90
|
-
* module doc for why. Identical to `TemporalSchedule` in
|
|
91
|
-
* `lexicons/temporal/src/resources.ts`.
|
|
92
|
-
*/
|
|
93
|
-
const ScheduleResource = createResource("Temporal::Schedule", "temporal", {});
|
|
94
|
-
|
|
95
|
-
/** `app-watch` becomes `appWatchWorkflow`, matching the temporal serializer's naming. */
|
|
96
|
-
function kebabToCamel(s: string): string {
|
|
97
|
-
return s.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
98
|
-
}
|
|
132
|
+
export type TerraformFindingMode = "report" | "issue" | "comment" | "pull-request";
|
|
99
133
|
|
|
100
134
|
export interface TerraformWatchOpConfig {
|
|
101
|
-
/** Op name (kebab-case).
|
|
135
|
+
/** Op name (kebab-case). */
|
|
102
136
|
name: string;
|
|
103
137
|
/** Key into the project's `terraform.roots`. The root carries dir, workspace, var files and backend config. */
|
|
104
138
|
root: string;
|
|
105
139
|
/**
|
|
106
|
-
* Cron expression. When set,
|
|
107
|
-
*
|
|
108
|
-
*
|
|
140
|
+
* Cron expression. When set, it lands on the Op as `schedule` — the cadence
|
|
141
|
+
* `chant operator` ticks on and `generateOpsPipeline` renders as a CI cron.
|
|
142
|
+
* Omit for one-shot `chant run` on the local executor.
|
|
109
143
|
*/
|
|
110
144
|
schedule?: string;
|
|
111
145
|
/**
|
|
112
146
|
* What to do when the plan proposes changes. Default: `"report"`, which
|
|
113
|
-
* opens nothing. The `Drift`
|
|
147
|
+
* opens nothing. The `Drift` run outcome and the run's own log are
|
|
114
148
|
* the report.
|
|
115
149
|
*/
|
|
116
150
|
findingMode?: TerraformFindingMode;
|
|
117
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* Watch a live root (#2105): the root runs `terraform.binary: "choudoufu"`
|
|
153
|
+
* and declares an estate, so the Plan phase is `choudoufuLivePlan` rather
|
|
154
|
+
* than `terraformPlan`. See the module doc for what changes.
|
|
155
|
+
*
|
|
156
|
+
* Declared here rather than detected, because a composite is built by
|
|
157
|
+
* `chant build` without reading a root module's `.tf` files. The activities
|
|
158
|
+
* detect the estate at run time (`op/activities/live-detect.ts`), but by then
|
|
159
|
+
* the phases are already serialized.
|
|
160
|
+
*
|
|
161
|
+
* Two things check the answer against the root's real mode, since a wrong
|
|
162
|
+
* one is otherwise silent (#2216). This function refuses a mismatch outright
|
|
163
|
+
* when it can resolve the mode synchronously, which is a
|
|
164
|
+
* `chant.config.json` project (`op/resolve-root-mode.ts`); TF028
|
|
165
|
+
* (`lint/post-synth/tf028.ts`) reports the same mismatch off the parsed HCL
|
|
166
|
+
* for every project, `chant.config.ts` included.
|
|
167
|
+
*/
|
|
168
|
+
live?: boolean;
|
|
169
|
+
/**
|
|
170
|
+
* The estate to plan against, for a live root whose estate this Op should
|
|
171
|
+
* name explicitly. Omitted, `choudoufuLivePlan` auto-detects it from the
|
|
172
|
+
* root's own `live` block or `estate.chdf.hcl` sidecar, which is the usual
|
|
173
|
+
* case. Refused on a stock root.
|
|
174
|
+
*/
|
|
175
|
+
estate?: string;
|
|
176
|
+
/**
|
|
177
|
+
* Plan file the Plan step writes, relative to the root dir. Default:
|
|
178
|
+
* `chant.tfplan`. Refused on a live root, where choudoufu takes no plan file
|
|
179
|
+
* at all.
|
|
180
|
+
*/
|
|
118
181
|
planFile?: string;
|
|
119
182
|
/** `-upgrade` on the Init step: re-resolve provider and module versions. */
|
|
120
183
|
upgrade?: boolean;
|
|
@@ -129,27 +192,72 @@ export interface TerraformWatchOpConfig {
|
|
|
129
192
|
title?: string;
|
|
130
193
|
/** Branch the pull-request mode opens from. Default: `reconcilePr`'s own. */
|
|
131
194
|
branch?: string;
|
|
132
|
-
/** Override the task queue. Defaults to `name`. */
|
|
133
|
-
taskQueue?: string;
|
|
134
195
|
}
|
|
135
196
|
|
|
136
197
|
export interface TerraformWatchOpResources {
|
|
137
|
-
/** Op resource. Generates the Init/Plan/[Report]
|
|
198
|
+
/** Op resource. Generates the Init/Plan/[Report] phases. */
|
|
138
199
|
op: InstanceType<typeof OpResource>;
|
|
139
|
-
/** `Temporal::Schedule`, present only when `schedule` was given. */
|
|
140
|
-
schedule?: Declarable;
|
|
141
200
|
}
|
|
142
201
|
|
|
143
202
|
export function TerraformWatchOp(config: TerraformWatchOpConfig): TerraformWatchOpResources {
|
|
144
|
-
const taskQueue = config.taskQueue ?? config.name;
|
|
145
|
-
const planFile = config.planFile ?? DEFAULT_PLAN_FILE;
|
|
146
203
|
const findingMode: TerraformFindingMode = config.findingMode ?? "report";
|
|
147
204
|
const where = config.cwd ? { cwd: config.cwd } : {};
|
|
148
205
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
206
|
+
if (config.live && config.planFile !== undefined) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`TerraformWatchOp "${config.name}": planFile is refused on a live root. choudoufu: ` +
|
|
209
|
+
`"${CHOUDOUFU_LIVE_PLAN_OUT_REFUSAL}". A live plan writes no saved plan, so there is no file to name; ` +
|
|
210
|
+
"drop planFile, or drop live if this root runs stock.",
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (!config.live && config.estate !== undefined) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`TerraformWatchOp "${config.name}": estate names the estate a live plan looks for markers of, and ` +
|
|
216
|
+
"this Op is not on a live root. Set live: true, or drop estate.",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The `live` flag against the root it names. Best-effort and synchronous:
|
|
221
|
+
// `resolveRootModeSync` reads a `chant.config.json` only and returns
|
|
222
|
+
// undefined for anything else, so a `chant.config.ts` project reaches
|
|
223
|
+
// neither branch and TF028 is what reports the mismatch there. Where the
|
|
224
|
+
// mode does resolve, refusing at build time names the Op, which a
|
|
225
|
+
// post-synth diagnostic does too but a run-time failure at the first step
|
|
226
|
+
// does not.
|
|
227
|
+
const resolvedMode = resolveRootModeSync(config.root, config.cwd)?.mode;
|
|
228
|
+
if (resolvedMode === "live" && !config.live) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`TerraformWatchOp "${config.name}": root "${config.root}" runs choudoufu with a declared estate, ` +
|
|
231
|
+
"but this Op watches it in stock mode. A stock plan reports drift alone and stays quiet about " +
|
|
232
|
+
"every unowned and adoptable resource in the estate. Set live: true.",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
if (resolvedMode === "state" && config.live) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`TerraformWatchOp "${config.name}": live: true, but root "${config.root}" declares no estate (or ` +
|
|
238
|
+
"terraform.binary is not \"choudoufu\"), so there is no live system to plan against. Drop " +
|
|
239
|
+
"live, or declare the root's estate.",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// `id` is what makes `plan.out` legal, and one field of `plan.out` is the
|
|
244
|
+
// only channel by which any part of the plan leaves this Op.
|
|
245
|
+
//
|
|
246
|
+
// A live root reads the live system instead of a state file, so the three
|
|
247
|
+
// things a watch reports come off one `choudoufuLivePlan`: `drift` from
|
|
248
|
+
// `-detailed-exitcode`, and `unowned`/`adoptable` from the `-json` document
|
|
249
|
+
// the same run wrote. Three attributes, one live read: see
|
|
250
|
+
// `ActivityStep.outcomeAttribute`'s array form (#2105).
|
|
251
|
+
const plan = config.live
|
|
252
|
+
? livePlanStep(config.root, { ...where, ...(config.estate ? { estate: config.estate } : {}), id: "plan" })
|
|
253
|
+
: planStep(config.root, { planFile: config.planFile ?? DEFAULT_PLAN_FILE, ...where, id: "plan" });
|
|
254
|
+
plan.outcomeAttribute = config.live
|
|
255
|
+
? [
|
|
256
|
+
{ name: "Drift", from: "drift" },
|
|
257
|
+
{ name: "Unowned", from: "unowned" },
|
|
258
|
+
{ name: "Adoptable", from: "adoptable" },
|
|
259
|
+
]
|
|
260
|
+
: { name: "Drift", from: "changed" };
|
|
153
261
|
|
|
154
262
|
const phases = [
|
|
155
263
|
phase("Init", [initStep(config.root, { ...where, ...(config.upgrade ? { upgrade: true } : {}) })]),
|
|
@@ -169,36 +277,36 @@ export function TerraformWatchOp(config: TerraformWatchOpConfig): TerraformWatch
|
|
|
169
277
|
mode: findingMode,
|
|
170
278
|
entries: [],
|
|
171
279
|
title: config.title ?? `Terraform drift in root "${config.root}"`,
|
|
172
|
-
|
|
280
|
+
// Stock: the `-no-color` plan. Live: the same plan text with the
|
|
281
|
+
// adoption ledger under it, which `choudoufuLivePlan` composes into
|
|
282
|
+
// one `finding` field precisely so that a body is one reference and
|
|
283
|
+
// the `-json` document has no path into one at all.
|
|
284
|
+
body: config.live ? plan.out.finding : plan.out.text,
|
|
173
285
|
...(config.branch ? { branch: config.branch } : {}),
|
|
174
286
|
},
|
|
175
287
|
outcomeAttribute:
|
|
176
|
-
findingMode === "pull-request"
|
|
288
|
+
findingMode === "pull-request"
|
|
289
|
+
? { name: "PR", from: "prUrl" }
|
|
290
|
+
: findingMode === "comment"
|
|
291
|
+
? { name: "Comment", from: "commentUrl" }
|
|
292
|
+
: { name: "Issue", from: "issueUrl" },
|
|
177
293
|
};
|
|
178
294
|
phases.push(phase("Report", [report]));
|
|
179
295
|
}
|
|
180
296
|
|
|
181
297
|
const op = Op({
|
|
182
298
|
name: config.name,
|
|
183
|
-
overview:
|
|
184
|
-
|
|
185
|
-
|
|
299
|
+
overview: config.live
|
|
300
|
+
? `Live-plan the "${config.root}" choudoufu estate and report drift, unowned and adoptable resources`
|
|
301
|
+
: `Plan the "${config.root}" terraform root and report drift`,
|
|
302
|
+
labels: {
|
|
186
303
|
Watch: "true",
|
|
187
304
|
TerraformRoot: config.root,
|
|
305
|
+
...(config.live ? { TerraformMode: "live" } : {}),
|
|
188
306
|
},
|
|
307
|
+
...(config.schedule ? { schedule: { cron: config.schedule, overlap: "skip" as const } } : {}),
|
|
189
308
|
phases,
|
|
190
309
|
});
|
|
191
310
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const schedule = new ScheduleResource({
|
|
195
|
-
scheduleId: `${config.name}-schedule`,
|
|
196
|
-
spec: { cronExpressions: [config.schedule] },
|
|
197
|
-
action: {
|
|
198
|
-
workflowType: kebabToCamel(config.name) + "Workflow",
|
|
199
|
-
taskQueue,
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
return { op, schedule };
|
|
311
|
+
return { op };
|
|
204
312
|
}
|
package/src/config.ts
CHANGED
|
@@ -25,6 +25,20 @@
|
|
|
25
25
|
import { z } from "zod";
|
|
26
26
|
import type { ChantConfig } from "@intentius/chant/config";
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* chant's cross-lexicon delete-mode vocabulary (`lexicons/k8s/src/op/
|
|
30
|
+
* activities/kubectl.ts`'s `ApplyDeleteMode`, `packages/core/src/op/
|
|
31
|
+
* activities/apply.ts`'s `DeleteMode`), read here for a live root and mapped
|
|
32
|
+
* onto choudoufu's `policy` block (#2106): `"never"` requires the root's
|
|
33
|
+
* `policy` to set `undeclared_tagged` to `"keep"`, `"untag"` or `"report"`
|
|
34
|
+
* (TF026 enforces this at build time); `"owned-only"` is choudoufu's own
|
|
35
|
+
* default verb for that quadrant (`delete`) and needs nothing; `"gated"`
|
|
36
|
+
* needs nothing beyond `TerraformApplyOp`'s own approval gate. Inert on a
|
|
37
|
+
* stock root: choudoufu's `policy` block does not exist there.
|
|
38
|
+
*/
|
|
39
|
+
export const terraformDeleteModeSchema = z.enum(["never", "owned-only", "gated"]);
|
|
40
|
+
export type TerraformDeleteMode = z.infer<typeof terraformDeleteModeSchema>;
|
|
41
|
+
|
|
28
42
|
/**
|
|
29
43
|
* One Terraform root module chant reads. `dir` is the only required field:
|
|
30
44
|
* everything else names how the root is invoked rather than what it declares.
|
|
@@ -43,14 +57,29 @@ export const terraformRootSchema = z.strictObject({
|
|
|
43
57
|
varFiles: z.array(z.string()).optional(),
|
|
44
58
|
/** `-backend-config` key/value pairs handed to `init`. */
|
|
45
59
|
backendConfig: z.record(z.string(), z.string()).optional(),
|
|
60
|
+
/** This root's declared delete mode. See {@link terraformDeleteModeSchema}. */
|
|
61
|
+
delete: terraformDeleteModeSchema.optional(),
|
|
46
62
|
});
|
|
47
63
|
|
|
48
64
|
export const terraformConfigSchema = z.strictObject({
|
|
49
65
|
/**
|
|
50
|
-
* Which CLI drives the roots.
|
|
51
|
-
* this lexicon does, so the choice is recorded rather than
|
|
66
|
+
* Which CLI drives the roots. `terraform` and `tofu` are wire-compatible
|
|
67
|
+
* for everything this lexicon does, so the choice is recorded rather than
|
|
68
|
+
* inferred. `choudoufu` (#2103) is an OpenTofu fork: a root running it also
|
|
69
|
+
* needs a declared estate (a `live` block or an `estate.chdf.hcl` sidecar,
|
|
70
|
+
* seen by the shared HCL parse) before it counts as live. Declaring the
|
|
71
|
+
* binary alone runs the root stock, exactly as `terraform`/`tofu` do.
|
|
72
|
+
*/
|
|
73
|
+
binary: z.enum(["terraform", "tofu", "choudoufu"]).optional(),
|
|
74
|
+
/**
|
|
75
|
+
* How far a root's parse follows its `module` calls (#2112), spelled the
|
|
76
|
+
* way tflint spells it. `local` (the default) reads modules sourced from a
|
|
77
|
+
* relative path, so TF014, TF015 and TF020 see inside them; `none` reads
|
|
78
|
+
* only each root's own directory; `all` is reserved for fetching registry
|
|
79
|
+
* and git modules and is refused with a message, since chant fetches
|
|
80
|
+
* nothing. See `hcl/descend.ts`.
|
|
52
81
|
*/
|
|
53
|
-
|
|
82
|
+
callModuleType: z.enum(["local", "none", "all"]).optional(),
|
|
54
83
|
/** Named root modules. The name is the entity-key prefix, so keep it stable. */
|
|
55
84
|
roots: z.record(z.string(), terraformRootSchema),
|
|
56
85
|
});
|