@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
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TerraformApplyOp composite tests (#2086) — the phase names, the gate, the
|
|
3
|
-
* plan-to-apply reference, and the compensation refusal.
|
|
3
|
+
* plan-to-apply reference, and the compensation refusal. A live root builds
|
|
4
|
+
* the same four phases (#2106 follow-up), and what is particular to it is
|
|
5
|
+
* below, in its own describe block.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
|
-
import {
|
|
8
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { afterEach, describe, test, expect } from "vitest";
|
|
7
12
|
import {
|
|
8
|
-
|
|
13
|
+
isGated,
|
|
9
14
|
isStepOutputRef,
|
|
10
15
|
runOpLocally,
|
|
11
|
-
|
|
16
|
+
memoryGateLedgerPort,
|
|
12
17
|
type ActivityStep,
|
|
13
18
|
type GateStep,
|
|
14
19
|
type OpConfig,
|
|
@@ -73,20 +78,28 @@ describe("TerraformApplyOp gate (#2086)", () => {
|
|
|
73
78
|
test("signal name defaults to approve-<name>", () => {
|
|
74
79
|
const op = props({ name: "prod-apply", root: "app" });
|
|
75
80
|
const gate = op.phases.find((p) => p.name === "Gate")!.steps.find(isGate)!;
|
|
76
|
-
expect(gate.
|
|
81
|
+
expect(gate.gate).toBe("approve-prod-apply");
|
|
77
82
|
});
|
|
78
83
|
|
|
79
|
-
|
|
84
|
+
// #2202: `gate` on this config already means the gate MODE, so the gate's
|
|
85
|
+
// name is `gateName` here. `signalName` is read through 0.59.0.
|
|
86
|
+
test("the deprecated `signalName` option still names the gate", () => {
|
|
87
|
+
const op = props({ name: "prod-apply", root: "app", signalName: "approve-terraform" });
|
|
88
|
+
const gate = op.phases.find((p) => p.name === "Gate")!.steps.find(isGate)!;
|
|
89
|
+
expect(gate.gate).toBe("approve-terraform");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("an explicit gateName, timeout and description win", () => {
|
|
80
93
|
const op = props({
|
|
81
94
|
name: "prod-apply",
|
|
82
95
|
root: "app",
|
|
83
|
-
|
|
96
|
+
gateName: "approve-terraform",
|
|
84
97
|
gateTimeout: "72h",
|
|
85
98
|
gateDescription: "Change window only",
|
|
86
99
|
});
|
|
87
100
|
const gate = op.phases.find((p) => p.name === "Gate")!.steps.find(isGate)!;
|
|
88
101
|
expect(gate).toMatchObject({
|
|
89
|
-
|
|
102
|
+
gate: "approve-terraform",
|
|
90
103
|
timeout: "72h",
|
|
91
104
|
description: "Change window only",
|
|
92
105
|
});
|
|
@@ -95,7 +108,7 @@ describe("TerraformApplyOp gate (#2086)", () => {
|
|
|
95
108
|
test("the Gate phase reports the plan's destroy count before it waits", () => {
|
|
96
109
|
// GateStep has no condition field, so "on-destroy" cannot branch at build
|
|
97
110
|
// time. The approver is told instead: a `show` of the saved plan runs
|
|
98
|
-
// first and surfaces `destroys` as a
|
|
111
|
+
// first and surfaces `destroys` as a run outcome.
|
|
99
112
|
const op = props({ name: "prod-apply", root: "app" });
|
|
100
113
|
const steps = op.phases.find((p) => p.name === "Gate")!.steps;
|
|
101
114
|
const show = steps[0] as ActivityStep;
|
|
@@ -168,26 +181,173 @@ describe("TerraformApplyOp compensation — total or refused (#2086)", () => {
|
|
|
168
181
|
});
|
|
169
182
|
});
|
|
170
183
|
|
|
171
|
-
describe("TerraformApplyOp on the local executor (#2086)", () => {
|
|
172
|
-
test("a gated Op
|
|
184
|
+
describe("TerraformApplyOp on the local executor (#2086, gate-as-fact #2119)", () => {
|
|
185
|
+
test("a gated Op stops at the gate and records the pending fact, naming the signal", async () => {
|
|
173
186
|
const op = props({ name: "prod-apply", root: "app" });
|
|
174
|
-
|
|
187
|
+
const ran: string[] = [];
|
|
188
|
+
const stub = (fn: string) => [fn, async () => { ran.push(fn); return {}; }] as const;
|
|
189
|
+
const activities = new Map(
|
|
190
|
+
["terraformInit", "terraformPlan", "terraformShow", "terraformApply"].map(stub),
|
|
191
|
+
);
|
|
192
|
+
const gates = memoryGateLedgerPort();
|
|
193
|
+
const result = await runOpLocally(op, activities, {}, undefined, { gates, now: "2026-09-05T12:00:00.000Z" });
|
|
194
|
+
|
|
195
|
+
expect(result.status).toBe("gated");
|
|
196
|
+
expect(result.gate?.op).toBe("prod-apply");
|
|
197
|
+
expect(result.gate?.gate).toBe(op.phases.flatMap((p) => p.steps).find(isGate)?.gate);
|
|
198
|
+
expect(gates.appended).toHaveLength(1);
|
|
199
|
+
// Init, Plan and the pre-gate `show` ran; Apply is behind the gate and did not.
|
|
200
|
+
expect(ran).toEqual(["terraformInit", "terraformPlan", "terraformShow"]);
|
|
175
201
|
});
|
|
176
202
|
|
|
177
|
-
test('gate: "never"
|
|
178
|
-
expect(
|
|
203
|
+
test('gate: "never" means there is no gate to stop at', () => {
|
|
204
|
+
expect(isGated(props({ name: "prod-apply", root: "app", gate: "never" }))).toBe(false);
|
|
205
|
+
expect(isGated(props({ name: "prod-apply", root: "app" }))).toBe(true);
|
|
179
206
|
});
|
|
180
207
|
});
|
|
181
208
|
|
|
182
209
|
describe("TerraformApplyOp Op metadata (#2086)", () => {
|
|
183
|
-
test("
|
|
210
|
+
test("labels name the root, and no task queue rides on the Op (#2118)", () => {
|
|
184
211
|
const op = props({ name: "app-apply", root: "app" });
|
|
185
212
|
expect(op.name).toBe("app-apply");
|
|
186
|
-
expect(op.
|
|
187
|
-
expect(op
|
|
213
|
+
expect(op.labels).toEqual({ Apply: "true", TerraformRoot: "app" });
|
|
214
|
+
expect(op).not.toHaveProperty("taskQueue");
|
|
188
215
|
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ── Live root (#2106) ────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
const dirs: string[] = [];
|
|
221
|
+
|
|
222
|
+
afterEach(() => {
|
|
223
|
+
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
/** A throwaway project whose one root is live, stock, or live with a policy block, per `policy`. */
|
|
227
|
+
function project(policy?: string): string {
|
|
228
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-tf-apply-op-live-"));
|
|
229
|
+
dirs.push(dir);
|
|
230
|
+
mkdirSync(join(dir, "root"), { recursive: true });
|
|
231
|
+
writeFileSync(
|
|
232
|
+
join(dir, "root", "main.tf"),
|
|
233
|
+
[
|
|
234
|
+
"terraform {",
|
|
235
|
+
" live {",
|
|
236
|
+
' estate = "fixture-estate"',
|
|
237
|
+
...(policy ? [" policy {", ` ${policy}`, " }"] : []),
|
|
238
|
+
" }",
|
|
239
|
+
"}",
|
|
240
|
+
"",
|
|
241
|
+
'resource "null_resource" "x" {}',
|
|
242
|
+
"",
|
|
243
|
+
].join("\n"),
|
|
244
|
+
);
|
|
245
|
+
writeFileSync(
|
|
246
|
+
join(dir, "chant.config.json"),
|
|
247
|
+
JSON.stringify({ terraform: { binary: "choudoufu", roots: { estate: { dir: "./root" } } } }),
|
|
248
|
+
);
|
|
249
|
+
return dir;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
describe("TerraformApplyOp on a live root (#2106 follow-up)", () => {
|
|
253
|
+
test("the same Init, Plan, Gate, Apply as a stock root, with the plan file paired through", () => {
|
|
254
|
+
const dir = project();
|
|
255
|
+
const op = props({ name: "estate-apply", root: "estate", cwd: dir });
|
|
256
|
+
expect(phaseNames(op)).toEqual(["Init", "Plan", "Gate", "Apply"]);
|
|
189
257
|
|
|
190
|
-
|
|
191
|
-
expect(
|
|
258
|
+
const activitySteps = op.phases.flatMap((p) => p.steps.filter(isActivity));
|
|
259
|
+
expect(activitySteps.map((s) => s.fn)).toEqual([
|
|
260
|
+
"terraformInit",
|
|
261
|
+
"terraformPlan",
|
|
262
|
+
"terraformShow",
|
|
263
|
+
"terraformApply",
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
const plan = op.phases.find((p) => p.name === "Plan")!.steps[0] as ActivityStep;
|
|
267
|
+
expect(plan.args?.planFile).toBe("chant.tfplan");
|
|
268
|
+
|
|
269
|
+
// The Apply step names the Plan step's own output, never a literal path:
|
|
270
|
+
// on a live root that reference is the approval artifact itself.
|
|
271
|
+
const apply = op.phases.find((p) => p.name === "Apply")!.steps[0] as ActivityStep;
|
|
272
|
+
expect(isStepOutputRef(apply.args?.planFile)).toBe(true);
|
|
273
|
+
expect(apply.args?.planFile).toMatchObject({ step: "plan", path: "planFile" });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("no choudoufuLivePlan step anywhere: the watch and adopt Ops still use it, this one does not", () => {
|
|
277
|
+
const dir = project();
|
|
278
|
+
const op = props({ name: "estate-apply", root: "estate", cwd: dir });
|
|
279
|
+
const fns = op.phases.flatMap((p) => p.steps.filter(isActivity)).map((s) => s.fn);
|
|
280
|
+
expect(fns).not.toContain("choudoufuLivePlan");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test('gate: "never" drops the Gate phase, leaving Init, Plan, Apply', () => {
|
|
284
|
+
const dir = project();
|
|
285
|
+
const op = props({ name: "estate-apply", root: "estate", cwd: dir, gate: "never" });
|
|
286
|
+
expect(phaseNames(op)).toEqual(["Init", "Plan", "Apply"]);
|
|
287
|
+
expect(op.phases.flatMap((p) => p.steps).some(isGate)).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("Plan reports Changed; the Gate's show step reports Destroys off the saved plan", () => {
|
|
291
|
+
const dir = project();
|
|
292
|
+
const op = props({ name: "estate-apply", root: "estate", cwd: dir });
|
|
293
|
+
const plan = op.phases.find((p) => p.name === "Plan")!.steps[0] as ActivityStep;
|
|
294
|
+
expect(plan.outcomeAttribute).toEqual({ name: "Changed", from: "changed" });
|
|
295
|
+
|
|
296
|
+
const gateSteps = op.phases.find((p) => p.name === "Gate")!.steps;
|
|
297
|
+
const show = gateSteps[0] as ActivityStep;
|
|
298
|
+
expect(show.fn).toBe("terraformShow");
|
|
299
|
+
expect(show.outcomeAttribute).toEqual({ name: "Destroys", from: "destroys" });
|
|
300
|
+
expect(gateSteps[1].kind).toBe("gate");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("the gate description tells the approver what the exit-3 refusal does", () => {
|
|
304
|
+
const dir = project();
|
|
305
|
+
const op = props({ name: "estate-apply", root: "estate", cwd: dir });
|
|
306
|
+
const gateStep = op.phases.find((p) => p.name === "Gate")!.steps.find(isGate)!;
|
|
307
|
+
expect(gateStep.description).toContain("live root");
|
|
308
|
+
expect(gateStep.description).toContain("exit status 3");
|
|
309
|
+
expect(gateStep.description).toContain("choudoufu #878");
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("a stock root's gate description says nothing about a live re-plan", () => {
|
|
313
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-tf-apply-op-live-"));
|
|
314
|
+
dirs.push(dir);
|
|
315
|
+
mkdirSync(join(dir, "root"), { recursive: true });
|
|
316
|
+
writeFileSync(join(dir, "root", "main.tf"), 'resource "null_resource" "x" {}\n');
|
|
317
|
+
writeFileSync(
|
|
318
|
+
join(dir, "chant.config.json"),
|
|
319
|
+
JSON.stringify({ terraform: { binary: "terraform", roots: { estate: { dir: "./root" } } } }),
|
|
320
|
+
);
|
|
321
|
+
const op = props({ name: "estate-apply", root: "estate", cwd: dir });
|
|
322
|
+
const gateStep = op.phases.find((p) => p.name === "Gate")!.steps.find(isGate)!;
|
|
323
|
+
expect(gateStep.description).not.toContain("exit status 3");
|
|
324
|
+
expect(gateStep.description).not.toContain("live root");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("refuses to build when the root's policy sets undeclared_untagged = \"delete\"", () => {
|
|
328
|
+
const dir = project('undeclared_untagged = "delete"');
|
|
329
|
+
expect(() => TerraformApplyOp({ name: "estate-apply", root: "estate", cwd: dir })).toThrow(
|
|
330
|
+
/TerraformApplyOp "estate-apply".*undeclared_untagged = "delete"/s,
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("builds fine when the root's policy sets undeclared_untagged to something else", () => {
|
|
335
|
+
const dir = project('undeclared_untagged = "report"');
|
|
336
|
+
expect(() => TerraformApplyOp({ name: "estate-apply", root: "estate", cwd: dir })).not.toThrow();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("a choudoufu root with no declared estate builds the same phases and skips the policy read", () => {
|
|
340
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-tf-apply-op-live-"));
|
|
341
|
+
dirs.push(dir);
|
|
342
|
+
mkdirSync(join(dir, "root"), { recursive: true });
|
|
343
|
+
writeFileSync(join(dir, "root", "main.tf"), 'resource "null_resource" "x" {}\n');
|
|
344
|
+
writeFileSync(
|
|
345
|
+
join(dir, "chant.config.json"),
|
|
346
|
+
JSON.stringify({ terraform: { binary: "choudoufu", roots: { estate: { dir: "./root" } } } }),
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const op = props({ name: "estate-apply", root: "estate", cwd: dir, gate: "never" });
|
|
350
|
+
const fns = op.phases.flatMap((p) => p.steps.filter(isActivity)).map((s) => s.fn);
|
|
351
|
+
expect(fns).toEqual(["terraformInit", "terraformPlan", "terraformApply"]);
|
|
192
352
|
});
|
|
193
353
|
});
|
|
@@ -1,17 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TerraformApplyOp composite (#2086)
|
|
2
|
+
* TerraformApplyOp composite (#2086), init, plan, approve, apply for one root
|
|
3
3
|
* module named in `terraform.roots`.
|
|
4
4
|
*
|
|
5
5
|
* Authority stays with terraform: chant runs the binary, reads its plan, and
|
|
6
|
-
* hands the same saved plan to apply. The Apply step never
|
|
7
|
-
* it takes the Plan step's own `planFile` as a
|
|
8
|
-
*
|
|
6
|
+
* hands the same saved plan to apply. The Apply step never applies something
|
|
7
|
+
* no gate saw, because it takes the Plan step's own `planFile` as a
|
|
8
|
+
* `StepOutputRef`.
|
|
9
9
|
*
|
|
10
|
-
* Phases: Init, Plan, Gate, Apply.
|
|
10
|
+
* Phases: Init, Plan, Gate, Apply. One shape, both kinds of root. A live root
|
|
11
|
+
* (#2103, `terraform.binary: "choudoufu"` plus a declared estate) ran a
|
|
12
|
+
* different shape for one release, because choudoufu refused `-out` and
|
|
13
|
+
* `apply <planfile>`; choudoufu v0.13.0 admits both under a `live` block
|
|
14
|
+
* ([choudoufu #878](https://github.com/INTENTIUS/choudoufu/issues/878),
|
|
15
|
+
* PR 889), so the plan-file discipline is back and the two shapes are one
|
|
16
|
+
* again.
|
|
11
17
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
18
|
+
* **What the approval covers on a live root.** `plan -out=<planFile>` writes
|
|
19
|
+
* stock's own plan file, and that file is what the approver reads and what
|
|
20
|
+
* the Apply step names. `apply <planFile>` does not replay it: prior state on
|
|
21
|
+
* a live root is a projection rebuilt from the live system every run, so the
|
|
22
|
+
* apply re-plans against the live system the way every live-markers run does,
|
|
23
|
+
* then compares its fresh plan with the file's. Same addresses, same actions,
|
|
24
|
+
* same live objects, and the same planned values, or it refuses before
|
|
25
|
+
* anything changes:
|
|
26
|
+
*
|
|
27
|
+
* - `The approved plan no longer matches the live system`, and
|
|
28
|
+
* - `The approved plan belongs to a different estate`,
|
|
29
|
+
*
|
|
30
|
+
* both with exit status **3**, which is neither 1 (any ordinary failure) nor
|
|
31
|
+
* `-detailed-exitcode`'s 2. `terraformApply` maps that status to a named
|
|
32
|
+
* result rather than a thrown error (`refused: "approval-mismatch" |
|
|
33
|
+
* "wrong-estate"`, with choudoufu's message in `refusal`), so a workflow
|
|
34
|
+
* reading the Apply step's output can route the run back to review instead of
|
|
35
|
+
* treating it as a broken step. The seam #2106 left named and unused on the
|
|
36
|
+
* apply args is now the plan file itself.
|
|
37
|
+
*
|
|
38
|
+
* TF101 therefore fires on a live root's `terraformApply` call again: there
|
|
39
|
+
* is a plan file to pair once more, and pairing it is what the refusal above
|
|
40
|
+
* has to compare against.
|
|
41
|
+
*
|
|
42
|
+
* **Delete modes.** chant's `delete: "never" | "owned-only" | "gated"`
|
|
43
|
+
* (`terraform.roots.<name>.delete`, `../config.ts`) maps onto choudoufu's
|
|
44
|
+
* `policy` block. `"owned-only"` is choudoufu's own default verb for the
|
|
45
|
+
* `undeclared_tagged` quadrant (an orphaned marked resource) and needs
|
|
46
|
+
* nothing; `"gated"` needs nothing beyond this composite's own approval gate;
|
|
47
|
+
* `"never"` requires the root's `policy` block to set `undeclared_tagged` to
|
|
48
|
+
* `"keep"`, `"untag"` or `"report"`, and TF026 (`../lint/post-synth/tf026.ts`)
|
|
49
|
+
* enforces that at build time, naming the setting to add. Separately, and
|
|
50
|
+
* regardless of `delete`, this composite refuses outright to build against a
|
|
51
|
+
* root whose `policy` sets `undeclared_untagged = "delete"` (account-scoped
|
|
52
|
+
* reconciliation, which needs a `scope` block): that quadrant deletes
|
|
53
|
+
* resources the estate never claimed, anywhere the scope reaches, and chant
|
|
54
|
+
* never proposes deleting a resource it does not own, not for a root, and not
|
|
55
|
+
* for an Op it generated.
|
|
56
|
+
*
|
|
57
|
+
* A gate is a fact on the gate ledger, not a wait (#2119). Reaching one,
|
|
58
|
+
* `chant run` consults the ledger (`packages/core/src/op/gate.ts`): a
|
|
59
|
+
* resolution newer than the gate's newest pending fact lets the run walk
|
|
60
|
+
* through, and anything else records the pending fact and ends the run with
|
|
61
|
+
* status `gated` and exit 3. `chant approve <op> <gate>` writes the
|
|
62
|
+
* resolution, and the next run reads it. `gate: "never"` drops the Gate phase
|
|
63
|
+
* entirely, so a run never stops.
|
|
15
64
|
*
|
|
16
65
|
* @example
|
|
17
66
|
* ```typescript
|
|
@@ -23,6 +72,8 @@
|
|
|
23
72
|
|
|
24
73
|
import { Op, phase, gate, activity, OpResource } from "@intentius/chant/op";
|
|
25
74
|
import { DEFAULT_PLAN_FILE } from "../op/activities/terraform";
|
|
75
|
+
import { detectLivePolicyVerbs } from "../op/activities/live-detect";
|
|
76
|
+
import { resolveRootModeSync } from "../op/resolve-root-mode";
|
|
26
77
|
import {
|
|
27
78
|
terraformInit as initStep,
|
|
28
79
|
terraformPlan as planStep,
|
|
@@ -38,14 +89,14 @@ import {
|
|
|
38
89
|
* cannot branch at build time on a count the plan only produces at run time.
|
|
39
90
|
* The workable v1 is that `"on-destroy"` and `"always"` emit the same Gate
|
|
40
91
|
* phase, and the approver is told what is at stake instead: the phase reports
|
|
41
|
-
* the plan's `destroys` count as the `Destroys`
|
|
42
|
-
*
|
|
43
|
-
* plan turns out additive is a new step kind, out of scope here.
|
|
92
|
+
* the plan's `destroys` count as the `Destroys` run outcome before the
|
|
93
|
+
* gate is reached, and the gate description says so. A gate that skips itself
|
|
94
|
+
* when the plan turns out additive is a new step kind, out of scope here.
|
|
44
95
|
*/
|
|
45
96
|
export type TerraformGateMode = "on-destroy" | "always" | "never";
|
|
46
97
|
|
|
47
98
|
export interface TerraformApplyOpConfig {
|
|
48
|
-
/** Op name (kebab-case).
|
|
99
|
+
/** Op name (kebab-case). `gateName` defaults to `approve-<name>`. */
|
|
49
100
|
name: string;
|
|
50
101
|
/** Key into the project's `terraform.roots`. The root carries dir, workspace, var files and backend config. */
|
|
51
102
|
root: string;
|
|
@@ -57,9 +108,15 @@ export interface TerraformApplyOpConfig {
|
|
|
57
108
|
* same phase.
|
|
58
109
|
*/
|
|
59
110
|
gate?: TerraformGateMode;
|
|
60
|
-
/**
|
|
111
|
+
/** The gate's name. Default: `approve-<name>`, as `ApplyOp` does. */
|
|
112
|
+
gateName?: string;
|
|
113
|
+
/**
|
|
114
|
+
* @deprecated The gate's name was `signalName` through 0.58.0 and is
|
|
115
|
+
* `gateName` since #2202 (`gate` is taken here by {@link TerraformGateMode}).
|
|
116
|
+
* Still read; removed in 0.60.0.
|
|
117
|
+
*/
|
|
61
118
|
signalName?: string;
|
|
62
|
-
/**
|
|
119
|
+
/** How long a recorded pending gate stays valid, as a duration string. Default: core's own (48h). */
|
|
63
120
|
gateTimeout?: string;
|
|
64
121
|
/** Override the gate description shown to the approver. */
|
|
65
122
|
gateDescription?: string;
|
|
@@ -78,15 +135,13 @@ export interface TerraformApplyOpConfig {
|
|
|
78
135
|
* Terraform has no automatic rollback: a half-applied plan is undone by
|
|
79
136
|
* planning and applying the inverse, which is a decision about the estate
|
|
80
137
|
* rather than something chant can synthesize. So compensation here is
|
|
81
|
-
* total or refused, the same stance `ApplyOp` takes (`
|
|
138
|
+
* total or refused, the same stance `ApplyOp` takes (`packages/core/src/op/
|
|
82
139
|
* composites/apply-op.ts`) — `true`, or an object with no `command`, throws
|
|
83
140
|
* when the Op is built, naming the Op, rather than warning at the moment a
|
|
84
141
|
* rollback is already needed. Supply `compensate: { command: "..." }` with a
|
|
85
142
|
* rollback of your own, or leave it unset.
|
|
86
143
|
*/
|
|
87
144
|
compensate?: boolean | { command?: string };
|
|
88
|
-
/** Override the task queue. Defaults to `name`. */
|
|
89
|
-
taskQueue?: string;
|
|
90
145
|
}
|
|
91
146
|
|
|
92
147
|
export interface TerraformApplyOpResources {
|
|
@@ -95,7 +150,6 @@ export interface TerraformApplyOpResources {
|
|
|
95
150
|
}
|
|
96
151
|
|
|
97
152
|
export function TerraformApplyOp(config: TerraformApplyOpConfig): TerraformApplyOpResources {
|
|
98
|
-
const taskQueue = config.taskQueue ?? config.name;
|
|
99
153
|
const planFile = config.planFile ?? DEFAULT_PLAN_FILE;
|
|
100
154
|
const gateMode: TerraformGateMode = config.gate ?? "on-destroy";
|
|
101
155
|
|
|
@@ -116,31 +170,71 @@ export function TerraformApplyOp(config: TerraformApplyOpConfig): TerraformApply
|
|
|
116
170
|
// travels with each of them rather than being read off the process once.
|
|
117
171
|
const where = config.cwd ? { cwd: config.cwd } : {};
|
|
118
172
|
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
173
|
+
// Best-effort, synchronous (chant.config.json only, see resolveRootModeSync's
|
|
174
|
+
// own doc comment). The mode no longer changes which steps are emitted; it
|
|
175
|
+
// words the gate below, and it decides whether the policy refusal is even
|
|
176
|
+
// asked. "Unknown" reads as stock, which for the gate wording is harmless
|
|
177
|
+
// and for the refusal is the permissive direction, so the refusal is not
|
|
178
|
+
// this function's to guarantee: on a chant.config.ts project, which is
|
|
179
|
+
// every project in this repository, the mode never resolves and the
|
|
180
|
+
// refusal below never runs (#2216).
|
|
181
|
+
const resolved = resolveRootModeSync(config.root, config.cwd);
|
|
182
|
+
const live = resolved?.mode === "live";
|
|
183
|
+
|
|
184
|
+
if (live) {
|
|
185
|
+
// Regardless of `delete`: an account-scoped purge is never something
|
|
186
|
+
// chant proposes on an Op's own initiative. TF026 handles the narrower,
|
|
187
|
+
// config-driven `delete: "never"` requirement; this is unconditional.
|
|
188
|
+
//
|
|
189
|
+
// TF027 (`../lint/post-synth/tf027.ts`) makes the same refusal off the
|
|
190
|
+
// parsed HCL, whatever the config file is written in, and is the check
|
|
191
|
+
// that guarantees it. This throw is kept for the projects whose mode does
|
|
192
|
+
// resolve here, where failing at the moment the Op is built names the Op
|
|
193
|
+
// as well as the root.
|
|
194
|
+
const verbs = detectLivePolicyVerbs(resolved!.dir);
|
|
195
|
+
if (verbs?.undeclaredUntagged === "delete") {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`TerraformApplyOp "${config.name}": root "${config.root}"'s policy block sets ` +
|
|
198
|
+
`undeclared_untagged = "delete" (account-scoped reconciliation, scoped by a \`scope\` block). ` +
|
|
199
|
+
"chant never proposes deleting a resource it does not own, and neither does an Op it " +
|
|
200
|
+
"generates. Remove that setting from the policy block, or narrow the estate's ownership " +
|
|
201
|
+
"answer instead of the account's.",
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
123
205
|
|
|
124
206
|
const phases = [
|
|
125
207
|
phase("Init", [initStep(config.root, { ...where, ...(config.upgrade ? { upgrade: true } : {}) })]),
|
|
126
|
-
phase("Plan", [plan]),
|
|
127
208
|
];
|
|
128
209
|
|
|
210
|
+
// `id` is what makes `plan.out` legal, and `plan.out.planFile` is how the
|
|
211
|
+
// Apply step below names this step's saved plan. On a live root that file
|
|
212
|
+
// is also the approval artifact the apply re-plans against, so the same
|
|
213
|
+
// reference carries the approval as well as the path.
|
|
214
|
+
const plan = planStep(config.root, { planFile, ...where, id: "plan" });
|
|
215
|
+
plan.outcomeAttribute = { name: "Changed", from: "changed" };
|
|
216
|
+
phases.push(phase("Plan", [plan]));
|
|
217
|
+
|
|
129
218
|
if (gateMode !== "never") {
|
|
130
|
-
// Re-render the saved plan just before the
|
|
219
|
+
// Re-render the saved plan just before the gate, so the approver's
|
|
131
220
|
// `Destroys` attribute comes off the plan that will actually apply.
|
|
132
|
-
// `show` against a plan file calls no provider.
|
|
221
|
+
// `show` against a plan file calls no provider, on either kind of root.
|
|
133
222
|
const show = showStep(config.root, { ...where, planFile: plan.out.planFile });
|
|
134
223
|
show.outcomeAttribute = { name: "Destroys", from: "destroys" };
|
|
135
224
|
phases.push(
|
|
136
225
|
phase("Gate", [
|
|
137
226
|
show,
|
|
138
|
-
gate(config.signalName ?? `approve-${config.name}`, {
|
|
227
|
+
gate(config.gateName ?? config.signalName ?? `approve-${config.name}`, {
|
|
139
228
|
...(config.gateTimeout ? { timeout: config.gateTimeout } : {}),
|
|
140
229
|
description:
|
|
141
230
|
config.gateDescription ??
|
|
142
|
-
`Approve terraform apply of root "${config.root}" (gate: ${gateMode}). ` +
|
|
143
|
-
`The Destroys
|
|
231
|
+
`Approve terraform apply of ${live ? "live " : ""}root "${config.root}" (gate: ${gateMode}). ` +
|
|
232
|
+
`The Destroys run outcome from this phase is the plan's destroy count.` +
|
|
233
|
+
(live
|
|
234
|
+
? " The apply that follows re-plans against the live system and applies this plan file only" +
|
|
235
|
+
" if its own fresh plan agrees; otherwise it refuses with exit status 3 and the Apply" +
|
|
236
|
+
" step reports a named refusal for review (choudoufu #878)."
|
|
237
|
+
: ""),
|
|
144
238
|
}),
|
|
145
239
|
]),
|
|
146
240
|
);
|
|
@@ -151,8 +245,7 @@ export function TerraformApplyOp(config: TerraformApplyOpConfig): TerraformApply
|
|
|
151
245
|
const op = Op({
|
|
152
246
|
name: config.name,
|
|
153
247
|
overview: `Init, plan and apply the "${config.root}" terraform root`,
|
|
154
|
-
|
|
155
|
-
searchAttributes: {
|
|
248
|
+
labels: {
|
|
156
249
|
Apply: "true",
|
|
157
250
|
TerraformRoot: config.root,
|
|
158
251
|
},
|