@intentius/chant-lexicon-terraform 0.58.0 → 0.60.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 +7 -2
- package/dist/composites/terraform-adopt-op.d.ts.map +1 -1
- package/dist/composites/terraform-apply-op.d.ts +8 -2
- package/dist/composites/terraform-apply-op.d.ts.map +1 -1
- package/dist/composites/terraform-watch-op.d.ts +30 -7
- package/dist/composites/terraform-watch-op.d.ts.map +1 -1
- package/dist/hcl/parse.d.ts +14 -0
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/integrity.json +7 -5
- package/dist/lint/audit-catalog.d.ts +2 -2
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +39 -12
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/tf001.d.ts +40 -6
- package/dist/lint/post-synth/tf001.d.ts.map +1 -1
- package/dist/lint/post-synth/tf015.d.ts +11 -2
- package/dist/lint/post-synth/tf015.d.ts.map +1 -1
- package/dist/lint/post-synth/tf026.d.ts +7 -0
- package/dist/lint/post-synth/tf026.d.ts.map +1 -1
- 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/lsp/option-keys.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +2 -0
- package/dist/okf/rules/TF027.md +11 -0
- package/dist/okf/rules/TF028.md +11 -0
- package/dist/op/activities/terraform.d.ts +32 -11
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/builders.d.ts +7 -5
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/resolve-root-mode.d.ts +29 -15
- package/dist/op/resolve-root-mode.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/tf001.ts +70 -14
- package/dist/rules/tf015.ts +11 -2
- package/dist/rules/tf026.ts +6 -2
- package/dist/rules/tf027.ts +74 -0
- package/dist/rules/tf028.ts +96 -0
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__fixtures__/ACCEPTANCE.md +181 -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 +18 -1
- package/src/composites/terraform-adopt-op.acceptance.test.ts +46 -19
- package/src/composites/terraform-adopt-op.test.ts +9 -4
- package/src/composites/terraform-adopt-op.ts +8 -3
- package/src/composites/terraform-apply-op.acceptance.test.ts +14 -9
- package/src/composites/terraform-apply-op.test.ts +13 -5
- package/src/composites/terraform-apply-op.ts +23 -7
- package/src/composites/terraform-watch-op.test.ts +85 -4
- package/src/composites/terraform-watch-op.ts +59 -8
- package/src/hcl/parse.ts +20 -0
- package/src/lint/audit-catalog.ts +26 -2
- package/src/lint/audit-lineage.ts +79 -14
- package/src/lint/audit.test.ts +171 -3
- package/src/lint/post-synth/fixtures/TF001/negative-cloud.tf +28 -0
- package/src/lint/post-synth/fixtures/TF001/negative.tf +4 -2
- package/src/lint/post-synth/fixtures/TF001/positive-local.tf +31 -0
- package/src/lint/post-synth/fixtures/TF014/negative/main.tf +2 -2
- package/src/lint/post-synth/fixtures/TF014/positive/main.tf +2 -2
- package/src/lint/post-synth/fixtures/TF020/scoped/main.tf +2 -2
- 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/index.ts +4 -0
- package/src/lint/post-synth/live-refusals-config-format.test.ts +143 -0
- package/src/lint/post-synth/module-rules.test.ts +1 -1
- package/src/lint/post-synth/post-synth.test.ts +64 -6
- package/src/lint/post-synth/tf001.ts +70 -14
- package/src/lint/post-synth/tf015.ts +11 -2
- package/src/lint/post-synth/tf026.ts +6 -2
- 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/lsp/option-keys.ts +3 -2
- package/src/op/activities/choudoufu.acceptance.test.ts +7 -23
- package/src/op/activities/choudoufu.test.ts +49 -17
- package/src/op/activities/terraform.ts +67 -29
- package/src/op/builders.ts +7 -5
- package/src/op/resolve-root-mode.ts +29 -15
- package/src/plugin.test.ts +38 -0
- package/src/plugin.ts +122 -31
- package/src/validate.ts +10 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# What the gated acceptance suites have actually run against
|
|
2
|
+
|
|
3
|
+
Four `describe` blocks across three files run this lexicon against a real
|
|
4
|
+
binary rather than a fixture, and every one of them skips rather than fails
|
|
5
|
+
when its dependency is absent. A skip is silent by design, so without this
|
|
6
|
+
file nothing in the tree says which of them has ever passed, against what, or
|
|
7
|
+
when. The evidence had been living in PR prose (`#2157` for the live apply
|
|
8
|
+
pair), which no reader of the suite can see.
|
|
9
|
+
|
|
10
|
+
One row per gated block below: what it proves, what gates it, the command,
|
|
11
|
+
and the last binary and emulator it is known to have passed against.
|
|
12
|
+
|
|
13
|
+
`live-estate/README.md` next door is the sibling record for the two recorded
|
|
14
|
+
documents (`../live-plan.json`, `../live-ls.json`) rather than for a suite,
|
|
15
|
+
and stays where it is.
|
|
16
|
+
|
|
17
|
+
## The binary and the emulator these rows name
|
|
18
|
+
|
|
19
|
+
`choudoufu v0.14.0` below means the published release binary, not a source
|
|
20
|
+
build: `gh release download v0.14.0 -R INTENTIUS/choudoufu -p
|
|
21
|
+
'choudoufu_v0.14.0_darwin_arm64.tar.gz' -p SHA256SUMS`, verified against
|
|
22
|
+
`SHA256SUMS` (`41c705d9b5fec47100c4f2fb9ab0694f0160b31a4e0661e877b4d9821bc464e3`),
|
|
23
|
+
extracted and put first on PATH. `choudoufu version` prints `choudoufu v0.14.0
|
|
24
|
+
(based on OpenTofu v1.13.0-dev)`.
|
|
25
|
+
|
|
26
|
+
`the pinned floci emulator` means choudoufu's own smoke stack image,
|
|
27
|
+
`ghcr.io/lex00/floci@sha256:a39185cc3971d0188663d61043cb038dff1260d8a975b1aa72c4e2bb1feac3cb`
|
|
28
|
+
(that checkout's `live/floci-image`), brought up from
|
|
29
|
+
`live/smoke/docker-compose.yml` and exported as
|
|
30
|
+
`CHOUDOUFU_EMULATOR_ENDPOINT=http://localhost:<mapped port>`.
|
|
31
|
+
|
|
32
|
+
## The four blocks
|
|
33
|
+
|
|
34
|
+
### 1. `TerraformApplyOp applies a real root`
|
|
35
|
+
|
|
36
|
+
`composites/terraform-apply-op.acceptance.test.ts`. One test: `runOpLocally`
|
|
37
|
+
drives `TerraformApplyOp` with `gate: "never"` through Init, Plan and Apply
|
|
38
|
+
over the `with-backend/` fixture in a temp directory, and two
|
|
39
|
+
`null_resource`s in the resulting state is the pass. The backend is `local`,
|
|
40
|
+
so no credentials and no remote state are involved.
|
|
41
|
+
|
|
42
|
+
Gated on a `terraform` or `tofu` on PATH, and on the provider download
|
|
43
|
+
reaching `registry.terraform.io` (`CHANT_OFFLINE` unset and the host
|
|
44
|
+
resolvable).
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
npx vitest run lexicons/terraform/src/composites/terraform-apply-op.acceptance.test.ts
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Last passed: 2026-09-07, `Terraform v1.15.8` on `darwin_arm64`, with
|
|
51
|
+
`hashicorp/null` resolved from the public registry for the fixture's `~> 3.2`
|
|
52
|
+
constraint. No emulator is involved. Re-run by chant #2168 alongside the two
|
|
53
|
+
live blocks in the same file, so the file's three rows all come from one run.
|
|
54
|
+
|
|
55
|
+
### 2. `TerraformApplyOp applies a live root against choudoufu's emulator`
|
|
56
|
+
|
|
57
|
+
Same file, second block. Two tests. The first is the live-root happy path end
|
|
58
|
+
to end through `runOpLocally`: Init, `plan -out=<file>`, `show` over that
|
|
59
|
+
file, `apply <file>`, two `null_resource`s applied against the `live/`
|
|
60
|
+
fixture. The second moves the world between plan and apply and asserts both
|
|
61
|
+
named refusals (`approval-mismatch`, `wrong-estate`) come back as results
|
|
62
|
+
carrying choudoufu's own message rather than as thrown errors, then applies
|
|
63
|
+
the same file once the world is restored.
|
|
64
|
+
|
|
65
|
+
Gated on a `choudoufu` on PATH, on it reporting a release version at or above
|
|
66
|
+
`MIN_CHOUDOUFU_VERSION` (0.14.0), and on `CHOUDOUFU_EMULATOR_ENDPOINT`. Not
|
|
67
|
+
gated on choudoufu #894 and never was: the plan half here is the stock `plan
|
|
68
|
+
-out` path and reads no JSON document.
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
npx vitest run lexicons/terraform/src/composites/terraform-apply-op.acceptance.test.ts
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Last passed: 2026-09-07, choudoufu v0.14.0 with the pinned floci emulator up,
|
|
75
|
+
recorded by chant #2168. That run is reproducible from this file: the binary
|
|
76
|
+
is a release asset and the emulator is a pinned digest. Before it, the only
|
|
77
|
+
record was PR #2157's prose ("the acceptance suite 3 passed against v0.13.0
|
|
78
|
+
with the emulator up", merged at `81200b5d97c15920619de4b109c4393ec3223720`),
|
|
79
|
+
which #2220 could not reproduce because no binary existed on the machine that
|
|
80
|
+
wrote this file.
|
|
81
|
+
|
|
82
|
+
### 3. `choudoufu live-check and live-plan against the fixture`
|
|
83
|
+
|
|
84
|
+
`op/activities/choudoufu.acceptance.test.ts`. Two tests. `live-check -json`
|
|
85
|
+
admits the `live/` fixture root (no cloud calls, so this half needs only the
|
|
86
|
+
binary), and `live-plan` reads the emulator and proposes creating both
|
|
87
|
+
`null_resource`s on a fresh estate.
|
|
88
|
+
|
|
89
|
+
Gated on a `choudoufu` on PATH and on `CHOUDOUFU_EMULATOR_ENDPOINT`. The
|
|
90
|
+
`CHOUDOUFU_894_OPEN` constant that used to skip the block whatever was on
|
|
91
|
+
PATH is gone.
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
npx vitest run lexicons/terraform/src/op/activities/choudoufu.acceptance.test.ts
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Last passed: 2026-09-07, choudoufu v0.14.0 with the pinned floci emulator up,
|
|
98
|
+
recorded by chant #2168. That is the first pass this block has ever had as a
|
|
99
|
+
suite, and the first the `live-plan` test has ever had at all. It had been
|
|
100
|
+
gated shut on choudoufu #894 since commit `65821d76` (2026-09-06 12:45Z);
|
|
101
|
+
before that gate, PR #2135's body records a hand verification of the
|
|
102
|
+
`live-check` half only.
|
|
103
|
+
|
|
104
|
+
Two chant-side changes in #2168 were what the pass needed, both consequences
|
|
105
|
+
of what choudoufu PR 915 shipped. `-estate` is still refused beside a declared
|
|
106
|
+
estate; what changed is that the flag is no longer needed, so
|
|
107
|
+
`choudoufuLivePlanCommand` omits it exactly when the configuration names its
|
|
108
|
+
own estate. And the human-render call carries `-detailed-exitcode` too, so
|
|
109
|
+
exit 2 there is a plan with changes rather than a failure, which is what the
|
|
110
|
+
first real run of this block found.
|
|
111
|
+
|
|
112
|
+
### 4. `TerraformAdoptOp adopts an unmarked live resource`
|
|
113
|
+
|
|
114
|
+
`composites/terraform-adopt-op.acceptance.test.ts`. One test: create an
|
|
115
|
+
unmarked VPC directly against the emulator so it is a live resource this
|
|
116
|
+
estate does not own at an identity the `live-adopt/` root declares, run the
|
|
117
|
+
Ledger step and expect exactly one adoptable match carrying the two marker
|
|
118
|
+
values, run the Adopt step and let it write them, then re-plan and expect the
|
|
119
|
+
estate to own the same VPC with nothing left adoptable.
|
|
120
|
+
|
|
121
|
+
Gated on a `choudoufu` on PATH, on an `aws` CLI on PATH (the unmarked
|
|
122
|
+
resource is created and adopted through it), on
|
|
123
|
+
`CHOUDOUFU_EMULATOR_ENDPOINT`, and then on
|
|
124
|
+
`CHOUDOUFU_ADOPTABLE_NOT_IN_DOCUMENT`, which replaced the #894 gate and names
|
|
125
|
+
choudoufu #962.
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
npx vitest run lexicons/terraform/src/composites/terraform-adopt-op.acceptance.test.ts
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Last passed: never, and the reason changed on 2026-09-07 rather than going
|
|
132
|
+
away. chant #2168 dropped the #894 gate and ran the block against choudoufu
|
|
133
|
+
v0.14.0 and the pinned floci emulator. It reached the document and stopped
|
|
134
|
+
there, with `ledger.adoptions` empty:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
AssertionError: expected [] to have a length of 1 but got +0
|
|
138
|
+
❯ terraform-adopt-op.acceptance.test.ts:116:34
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The document's `unowned[]` is the resources found at an identity the
|
|
142
|
+
configuration itself declares, and it works: an unmarked
|
|
143
|
+
`aws_cloudwatch_log_group` in the same fixture shape comes back on the same
|
|
144
|
+
binary with `adopt_tofu_estate` and `adopt_tofu_address` on it, which is the
|
|
145
|
+
shape `../live-plan.json` recorded. An `aws_vpc` has no such identity, so the
|
|
146
|
+
document reports `omissions[].reason = "NEEDS_DISCOVERY"` and leaves
|
|
147
|
+
`unowned[]` empty. The VPC is matched by choudoufu's content matcher during
|
|
148
|
+
the estate-wide unclaimed sweep instead, and that match is printed only in the
|
|
149
|
+
human render's "Adoptable" section, for which `views.LivePlanDocument` has no
|
|
150
|
+
field. `-adoption-only` is refused alongside `-json`, and
|
|
151
|
+
`TOFU_LIVE_COLLECT_UNCLAIMED=1` on the `-json` run leaves `"unowned": []`
|
|
152
|
+
while the text run beside it prints `Adoptable: 1 live resource matches a
|
|
153
|
+
declared resource`. Filed upstream as choudoufu #962
|
|
154
|
+
(https://github.com/INTENTIUS/choudoufu/issues/962), which carries the full
|
|
155
|
+
measurement; chant #2168 has the same.
|
|
156
|
+
|
|
157
|
+
The fixture was deliberately left as an `aws_vpc`. A log group would make the
|
|
158
|
+
block pass and would stop it proving the content-matcher path, which is the
|
|
159
|
+
only thing it exists to prove.
|
|
160
|
+
|
|
161
|
+
## Running everything that can run on a machine with no choudoufu
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
npx vitest run \
|
|
165
|
+
lexicons/terraform/src/composites/terraform-apply-op.acceptance.test.ts \
|
|
166
|
+
lexicons/terraform/src/composites/terraform-adopt-op.acceptance.test.ts \
|
|
167
|
+
lexicons/terraform/src/op/activities/choudoufu.acceptance.test.ts
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
With `terraform` on PATH and the registry reachable, that is 1 passed and 5
|
|
171
|
+
skipped, and each skip names its own reason in the block title. With
|
|
172
|
+
choudoufu v0.14.0, the `aws` CLI and the emulator all present, it is 5 passed
|
|
173
|
+
and 1 skipped: block 4 is the skip. On 2026-09-07 that is exactly what it
|
|
174
|
+
printed.
|
|
175
|
+
|
|
176
|
+
## Keeping this file honest
|
|
177
|
+
|
|
178
|
+
Add a row when a suite is added, and update a "last passed" line in the same
|
|
179
|
+
change that makes a run happen. A line here that names no binary version and
|
|
180
|
+
no date is worth less than no line at all, so if a run cannot be reproduced,
|
|
181
|
+
say whose prose the claim comes from and that it was not reproduced.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# The `with-backend` root next door keeps its state local on purpose, so
|
|
2
|
+
# TF001 reports it (#2218). This one is the same configuration with a remote
|
|
3
|
+
# backend, and is what a root TF001 says nothing about looks like. Nothing
|
|
4
|
+
# ever runs `terraform init` here, so the bucket need not exist.
|
|
5
|
+
terraform {
|
|
6
|
+
required_version = ">= 1.5.0"
|
|
7
|
+
|
|
8
|
+
backend "s3" {
|
|
9
|
+
bucket = "tfstate"
|
|
10
|
+
key = "app/terraform.tfstate"
|
|
11
|
+
region = "us-east-1"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
required_providers {
|
|
15
|
+
null = {
|
|
16
|
+
source = "hashicorp/null"
|
|
17
|
+
version = "~> 3.2"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
provider "null" {}
|
|
23
|
+
|
|
24
|
+
resource "null_resource" "first" {
|
|
25
|
+
triggers = {
|
|
26
|
+
name = "first"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
resource "null_resource" "second" {
|
|
31
|
+
triggers = {
|
|
32
|
+
name = "second"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# The `terraform-apply-op` acceptance test copies this root into a temp
|
|
2
|
+
# directory and runs a real `terraform init`, `plan` and `apply` against it,
|
|
3
|
+
# so the backend has to be one that works with no credentials and no network
|
|
4
|
+
# beyond the provider registry. That is `local`, which is why TF001 reports
|
|
5
|
+
# this root (#2218): the finding is correct, and the local state is the point
|
|
6
|
+
# of the fixture. `remote-backend/` next door is the same configuration with
|
|
7
|
+
# a backend TF001 says nothing about.
|
|
1
8
|
terraform {
|
|
2
9
|
required_version = ">= 1.5.0"
|
|
3
10
|
|
|
@@ -3,6 +3,7 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { describe, expect, test } from "vitest";
|
|
5
5
|
import { scanRules } from "@intentius/chant/codegen/docs-rule-scanning";
|
|
6
|
+
import { RULE_CATALOG } from "@intentius/chant/audit/catalog";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* `docs/pages/lint-rules.mdx` is authored by hand, one `##` section per rule
|
|
@@ -14,9 +15,25 @@ import { scanRules } from "@intentius/chant/codegen/docs-rule-scanning";
|
|
|
14
15
|
*/
|
|
15
16
|
const pkgDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* The TF ids core owns rather than this lexicon (#2220). `scanRules` walks
|
|
20
|
+
* this package's `src/`, so it cannot see them: TF023 (Terraform state
|
|
21
|
+
* committed to the repository) lives in `packages/core/src/audit/catalog.ts`
|
|
22
|
+
* because it reads the discovered file list rather than a parsed root module,
|
|
23
|
+
* and its page section here was the one TF section nothing guarded. Read off
|
|
24
|
+
* `RULE_CATALOG` rather than hardcoded, so a second core-owned TF id is
|
|
25
|
+
* covered on the day it lands.
|
|
26
|
+
*/
|
|
27
|
+
function coreOwnedTfIds(): string[] {
|
|
28
|
+
return Object.keys(RULE_CATALOG).filter((id) => /^TF\d+$/.test(id));
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
describe("lint-rules.mdx stays in sync with the rule catalog", () => {
|
|
18
32
|
test("every rule id scanned from source has a ## section on the authored page", () => {
|
|
19
|
-
const
|
|
33
|
+
const core = coreOwnedTfIds();
|
|
34
|
+
expect(core, "core owns at least TF023").toContain("TF023");
|
|
35
|
+
|
|
36
|
+
const ids = [...new Set([...scanRules(join(pkgDir, "src")).map((r) => r.id), ...core])].sort();
|
|
20
37
|
expect(ids.length).toBeGreaterThan(0);
|
|
21
38
|
|
|
22
39
|
const page = readFileSync(join(pkgDir, "docs", "pages", "lint-rules.mdx"), "utf-8");
|
|
@@ -20,25 +20,46 @@
|
|
|
20
20
|
* hand-off, which is the part a real binary can falsify. `choudoufu.acceptance.test.ts` takes the same approach for the same
|
|
21
21
|
* reason.
|
|
22
22
|
*
|
|
23
|
-
* ## Why this skips
|
|
23
|
+
* ## Why this still skips on choudoufu v0.14.0
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* own docs lead with is the one shape the #788 document cannot be produced
|
|
31
|
-
* for, so `choudoufuLivePlan` throws on any real live root until that lands.
|
|
32
|
-
* Found by chant #2104 against a source build at HEAD.
|
|
25
|
+
* Not #894 any more. v0.14.0 (choudoufu PR 915) made `live-plan -json`
|
|
26
|
+
* reachable on a configuration that names its own estate, and this suite was
|
|
27
|
+
* run against it on 2026-09-07 for the first time. It got as far as the
|
|
28
|
+
* document and stopped there: `ledger.adoptions` came back empty, because the
|
|
29
|
+
* #788 document carries no adoptable-by-content section at all.
|
|
33
30
|
*
|
|
34
|
-
* The
|
|
35
|
-
*
|
|
36
|
-
*
|
|
31
|
+
* The document's `unowned[]` is the resources found at an identity the
|
|
32
|
+
* configuration itself declares. A `aws_cloudwatch_log_group` has one (its
|
|
33
|
+
* name is in the block), so an unmarked live one comes back in `unowned[]`
|
|
34
|
+
* with `adopt_tofu_estate`/`adopt_tofu_address` on it, which is exactly what
|
|
35
|
+
* `../__fixtures__/live-plan.json` recorded and what `readAdoptionLedger`
|
|
36
|
+
* reads. An `aws_vpc` has none: EC2 assigns the id, so the document reports
|
|
37
|
+
* `omissions[].reason = "NEEDS_DISCOVERY"` and leaves `unowned[]` empty. The
|
|
38
|
+
* VPC is matched instead by choudoufu's content matcher during the
|
|
39
|
+
* estate-wide unclaimed sweep, and that match is printed only in the human
|
|
40
|
+
* render's "Adoptable" section. `views.LivePlanDocument` has no field for it,
|
|
41
|
+
* `-adoption-only` is refused alongside `-json` ("-adoption-only and -json
|
|
42
|
+
* cannot be combined"), and `TOFU_LIVE_COLLECT_UNCLAIMED=1` on the `-json`
|
|
43
|
+
* run makes no difference: the text render then prints "Adoptable: 1 live
|
|
44
|
+
* resource matches a declared resource" and the document beside it still says
|
|
45
|
+
* `"unowned": []`.
|
|
46
|
+
*
|
|
47
|
+
* So `choudoufuLivePlan` cannot produce a ledger for a provider-assigned
|
|
48
|
+
* identity on any binary that exists today, and this suite is what would
|
|
49
|
+
* prove it can. Filed upstream as
|
|
50
|
+
* [choudoufu #962](https://github.com/INTENTIUS/choudoufu/issues/962); the
|
|
51
|
+
* measurements are there and on chant #2168.
|
|
52
|
+
*
|
|
53
|
+
* The gate below names that, alongside the three ordinary dependencies: a
|
|
54
|
+
* `choudoufu`, an `aws` CLI, and the emulator's endpoint.
|
|
37
55
|
*
|
|
38
56
|
* Gating copied from `./terraform-apply-op.acceptance.test.ts` (`onPath`),
|
|
39
57
|
* which in turn copies `lexicons/k3s/src/serializer.acceptance.test.ts`'s
|
|
40
58
|
* pattern of skipping with the reason named in the describe title rather than
|
|
41
59
|
* failing when the real dependency is absent.
|
|
60
|
+
*
|
|
61
|
+
* `../__fixtures__/ACCEPTANCE.md` records what this block has last passed
|
|
62
|
+
* against, which is still nothing, and why the reason changed.
|
|
42
63
|
*/
|
|
43
64
|
|
|
44
65
|
import { execSync } from "node:child_process";
|
|
@@ -58,11 +79,16 @@ function onPath(cmd: string): boolean {
|
|
|
58
79
|
}
|
|
59
80
|
|
|
60
81
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
82
|
+
* `true` while `live-plan -json`'s document carries no adoptable-by-content
|
|
83
|
+
* section, so a provider-assigned identity like an `aws_vpc` never reaches
|
|
84
|
+
* `readAdoptionLedger`. Filed upstream as
|
|
85
|
+
* [choudoufu #962](https://github.com/INTENTIUS/choudoufu/issues/962), with
|
|
86
|
+
* the measurements against the v0.14.0 release binary on 2026-09-07; flip to
|
|
87
|
+
* `false` when a choudoufu release puts the content matcher's "Adoptable"
|
|
88
|
+
* rows in the document, either in `unowned[]` or in a sibling array.
|
|
89
|
+
* choudoufu #894, which gated this block before, is fixed and gone.
|
|
64
90
|
*/
|
|
65
|
-
const
|
|
91
|
+
const CHOUDOUFU_ADOPTABLE_NOT_IN_DOCUMENT = true;
|
|
66
92
|
|
|
67
93
|
const emulatorEndpoint = process.env.CHOUDOUFU_EMULATOR_ENDPOINT;
|
|
68
94
|
|
|
@@ -72,9 +98,10 @@ const skipReason = !onPath("choudoufu")
|
|
|
72
98
|
? "no aws CLI on PATH (the unmarked resource is created with it, and adopted through it)"
|
|
73
99
|
: !emulatorEndpoint
|
|
74
100
|
? "CHOUDOUFU_EMULATOR_ENDPOINT is not set (bring up choudoufu's `just smoke` emulator stack and export it)"
|
|
75
|
-
:
|
|
76
|
-
? "choudoufu#
|
|
77
|
-
"
|
|
101
|
+
: CHOUDOUFU_ADOPTABLE_NOT_IN_DOCUMENT
|
|
102
|
+
? "choudoufu#962: live-plan -json's document carries no adoptable-by-content section, so the " +
|
|
103
|
+
"fixture's unmarked aws_vpc reaches omissions[NEEDS_DISCOVERY] and never unowned[]; " +
|
|
104
|
+
"measured on choudoufu v0.14.0"
|
|
78
105
|
: "";
|
|
79
106
|
|
|
80
107
|
const FIXTURE = join(import.meta.dirname, "..", "__fixtures__", "live-adopt");
|
|
@@ -80,7 +80,7 @@ describe("TerraformAdoptOp gates on the ledger (#2105)", () => {
|
|
|
80
80
|
// Unlike TerraformApplyOp there is no `gate: "never"` here. Adoption moves
|
|
81
81
|
// the estate's boundary onto resources it did not own, which is not a
|
|
82
82
|
// thing to do unattended.
|
|
83
|
-
expect(gateOf(props(base)).
|
|
83
|
+
expect(gateOf(props(base)).gate).toBe("approve-estate-adopt");
|
|
84
84
|
expect(Object.keys(TerraformAdoptOp(base))).toEqual(["op"]);
|
|
85
85
|
});
|
|
86
86
|
|
|
@@ -91,9 +91,14 @@ describe("TerraformAdoptOp gates on the ledger (#2105)", () => {
|
|
|
91
91
|
expect(description).toContain("an ambiguous address is never adopted");
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
expect(
|
|
94
|
+
// #2202: `gateName` is the option; `signalName` is read through 0.59.0.
|
|
95
|
+
test("the deprecated `signalName` option still names the gate", () => {
|
|
96
|
+
expect(gateOf(props({ ...base, signalName: "ok" })).gate).toBe("ok");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("gate name, timeout and description are overridable", () => {
|
|
100
|
+
const gate = gateOf(props({ ...base, gateName: "ok", gateTimeout: "2h", gateDescription: "mine" }));
|
|
101
|
+
expect(gate).toMatchObject({ gate: "ok", timeout: "2h", description: "mine" });
|
|
97
102
|
});
|
|
98
103
|
|
|
99
104
|
test("no timeout given means the gate carries none, so core's own default applies", () => {
|
|
@@ -89,7 +89,7 @@ import {
|
|
|
89
89
|
} from "../op/builders";
|
|
90
90
|
|
|
91
91
|
export interface TerraformAdoptOpConfig {
|
|
92
|
-
/** Op name (kebab-case). `
|
|
92
|
+
/** Op name (kebab-case). `gateName` defaults to `approve-<name>`. */
|
|
93
93
|
name: string;
|
|
94
94
|
/** Key into the project's `terraform.roots`. Must be a live root: choudoufu, with a declared estate. */
|
|
95
95
|
root: string;
|
|
@@ -99,7 +99,12 @@ export interface TerraformAdoptOpConfig {
|
|
|
99
99
|
* sidecar, which is the usual case.
|
|
100
100
|
*/
|
|
101
101
|
estate?: string;
|
|
102
|
-
/**
|
|
102
|
+
/** The gate's name. Default: `approve-<name>`, as `TerraformApplyOp` does. */
|
|
103
|
+
gateName?: string;
|
|
104
|
+
/**
|
|
105
|
+
* @deprecated The gate's name was `signalName` through 0.58.0 and is
|
|
106
|
+
* `gateName` since #2202, matching `TerraformApplyOp`. Removed in 0.60.0.
|
|
107
|
+
*/
|
|
103
108
|
signalName?: string;
|
|
104
109
|
/** How long a recorded pending gate stays valid, as a duration string. Default: core's own (48h). */
|
|
105
110
|
gateTimeout?: string;
|
|
@@ -176,7 +181,7 @@ export function TerraformAdoptOp(config: TerraformAdoptOpConfig): TerraformAdopt
|
|
|
176
181
|
phase("Check", [check]),
|
|
177
182
|
phase("Ledger", [ledger]),
|
|
178
183
|
phase("Gate", [
|
|
179
|
-
gate(config.signalName ?? `approve-${config.name}`, {
|
|
184
|
+
gate(config.gateName ?? config.signalName ?? `approve-${config.name}`, {
|
|
180
185
|
...(config.gateTimeout ? { timeout: config.gateTimeout } : {}),
|
|
181
186
|
description:
|
|
182
187
|
config.gateDescription ??
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Gating copied from `lexicons/k3s/src/serializer.acceptance.test.ts`, which
|
|
22
22
|
* skips the same way when Docker is absent.
|
|
23
|
+
*
|
|
24
|
+
* `../__fixtures__/ACCEPTANCE.md` records what both blocks in this file have
|
|
25
|
+
* last passed against, with the binary version and the date (#2220). Update
|
|
26
|
+
* it in the same change as a run.
|
|
23
27
|
*/
|
|
24
28
|
|
|
25
29
|
import { execSync } from "node:child_process";
|
|
@@ -152,18 +156,19 @@ describe.skipIf(skipReason !== "")(
|
|
|
152
156
|
* `../op/activities/choudoufu.acceptance.test.ts` is, plus a version floor:
|
|
153
157
|
*
|
|
154
158
|
* - no `choudoufu` on PATH,
|
|
155
|
-
* - a `choudoufu` older than {@link MIN_CHOUDOUFU_VERSION}
|
|
156
|
-
*
|
|
159
|
+
* - a `choudoufu` older than {@link MIN_CHOUDOUFU_VERSION}: v0.13.0 shipped
|
|
160
|
+
* the approval artifact
|
|
157
161
|
* ([choudoufu #878](https://github.com/INTENTIUS/choudoufu/issues/878),
|
|
158
|
-
* PR 889)
|
|
159
|
-
* Op could not be built the way it is built now,
|
|
162
|
+
* PR 889), before which `plan -out` was refused under a live block and
|
|
163
|
+
* this Op could not be built the way it is built now, and v0.14.0 moved
|
|
164
|
+
* the floor again for the document the other two Ops read
|
|
165
|
+
* ([choudoufu #894](https://github.com/INTENTIUS/choudoufu/issues/894)),
|
|
160
166
|
* - `CHOUDOUFU_EMULATOR_ENDPOINT` unset (bring up choudoufu's `just smoke`
|
|
161
167
|
* docker compose stack and export `http://localhost:<mapped port>`).
|
|
162
168
|
*
|
|
163
|
-
* The #894 clause the first version of this suite carried is gone
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* and `TerraformAdoptOp` still need the document and still wait on it.
|
|
169
|
+
* The #894 clause the first version of this suite carried is gone, and stayed
|
|
170
|
+
* gone: that issue was about `live-plan -json`, which the apply Op does not
|
|
171
|
+
* run at all, since the plan half is the stock `plan -out` path.
|
|
167
172
|
*
|
|
168
173
|
* The first test is the happy path end to end through `runOpLocally`. The
|
|
169
174
|
* second is the refusal, and it runs the activities directly rather than
|
|
@@ -213,7 +218,7 @@ const liveSkipReason: string = !onPath("choudoufu")
|
|
|
213
218
|
: choudoufuVersion === undefined
|
|
214
219
|
? "the choudoufu on PATH reports no release version (a dev build), so the approval artifact cannot be assumed"
|
|
215
220
|
: isOlderVersion(choudoufuVersion, MIN_CHOUDOUFU_VERSION)
|
|
216
|
-
? `choudoufu ${choudoufuVersion} is older than v${MIN_CHOUDOUFU_VERSION},
|
|
221
|
+
? `choudoufu ${choudoufuVersion} is older than v${MIN_CHOUDOUFU_VERSION}, the lexicon's floor (choudoufu #878's approval artifact in v0.13.0, #894's -json document in v0.14.0)`
|
|
217
222
|
: !emulatorEndpoint
|
|
218
223
|
? "CHOUDOUFU_EMULATOR_ENDPOINT is not set (bring up choudoufu's `just smoke` emulator stack and export it)"
|
|
219
224
|
: "";
|
|
@@ -78,20 +78,28 @@ describe("TerraformApplyOp gate (#2086)", () => {
|
|
|
78
78
|
test("signal name defaults to approve-<name>", () => {
|
|
79
79
|
const op = props({ name: "prod-apply", root: "app" });
|
|
80
80
|
const gate = op.phases.find((p) => p.name === "Gate")!.steps.find(isGate)!;
|
|
81
|
-
expect(gate.
|
|
81
|
+
expect(gate.gate).toBe("approve-prod-apply");
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
|
|
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", () => {
|
|
85
93
|
const op = props({
|
|
86
94
|
name: "prod-apply",
|
|
87
95
|
root: "app",
|
|
88
|
-
|
|
96
|
+
gateName: "approve-terraform",
|
|
89
97
|
gateTimeout: "72h",
|
|
90
98
|
gateDescription: "Change window only",
|
|
91
99
|
});
|
|
92
100
|
const gate = op.phases.find((p) => p.name === "Gate")!.steps.find(isGate)!;
|
|
93
101
|
expect(gate).toMatchObject({
|
|
94
|
-
|
|
102
|
+
gate: "approve-terraform",
|
|
95
103
|
timeout: "72h",
|
|
96
104
|
description: "Change window only",
|
|
97
105
|
});
|
|
@@ -186,7 +194,7 @@ describe("TerraformApplyOp on the local executor (#2086, gate-as-fact #2119)", (
|
|
|
186
194
|
|
|
187
195
|
expect(result.status).toBe("gated");
|
|
188
196
|
expect(result.gate?.op).toBe("prod-apply");
|
|
189
|
-
expect(result.gate?.gate).toBe(op.phases.flatMap((p) => p.steps).find(isGate)?.
|
|
197
|
+
expect(result.gate?.gate).toBe(op.phases.flatMap((p) => p.steps).find(isGate)?.gate);
|
|
190
198
|
expect(gates.appended).toHaveLength(1);
|
|
191
199
|
// Init, Plan and the pre-gate `show` ran; Apply is behind the gate and did not.
|
|
192
200
|
expect(ran).toEqual(["terraformInit", "terraformPlan", "terraformShow"]);
|
|
@@ -96,7 +96,7 @@ import {
|
|
|
96
96
|
export type TerraformGateMode = "on-destroy" | "always" | "never";
|
|
97
97
|
|
|
98
98
|
export interface TerraformApplyOpConfig {
|
|
99
|
-
/** Op name (kebab-case). `
|
|
99
|
+
/** Op name (kebab-case). `gateName` defaults to `approve-<name>`. */
|
|
100
100
|
name: string;
|
|
101
101
|
/** Key into the project's `terraform.roots`. The root carries dir, workspace, var files and backend config. */
|
|
102
102
|
root: string;
|
|
@@ -108,7 +108,13 @@ export interface TerraformApplyOpConfig {
|
|
|
108
108
|
* same phase.
|
|
109
109
|
*/
|
|
110
110
|
gate?: TerraformGateMode;
|
|
111
|
-
/**
|
|
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
|
+
*/
|
|
112
118
|
signalName?: string;
|
|
113
119
|
/** How long a recorded pending gate stays valid, as a duration string. Default: core's own (48h). */
|
|
114
120
|
gateTimeout?: string;
|
|
@@ -164,10 +170,14 @@ export function TerraformApplyOp(config: TerraformApplyOpConfig): TerraformApply
|
|
|
164
170
|
// travels with each of them rather than being read off the process once.
|
|
165
171
|
const where = config.cwd ? { cwd: config.cwd } : {};
|
|
166
172
|
|
|
167
|
-
// Best-effort, synchronous (chant.config.json only
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
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).
|
|
171
181
|
const resolved = resolveRootModeSync(config.root, config.cwd);
|
|
172
182
|
const live = resolved?.mode === "live";
|
|
173
183
|
|
|
@@ -175,6 +185,12 @@ export function TerraformApplyOp(config: TerraformApplyOpConfig): TerraformApply
|
|
|
175
185
|
// Regardless of `delete`: an account-scoped purge is never something
|
|
176
186
|
// chant proposes on an Op's own initiative. TF026 handles the narrower,
|
|
177
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.
|
|
178
194
|
const verbs = detectLivePolicyVerbs(resolved!.dir);
|
|
179
195
|
if (verbs?.undeclaredUntagged === "delete") {
|
|
180
196
|
throw new Error(
|
|
@@ -208,7 +224,7 @@ export function TerraformApplyOp(config: TerraformApplyOpConfig): TerraformApply
|
|
|
208
224
|
phases.push(
|
|
209
225
|
phase("Gate", [
|
|
210
226
|
show,
|
|
211
|
-
gate(config.signalName ?? `approve-${config.name}`, {
|
|
227
|
+
gate(config.gateName ?? config.signalName ?? `approve-${config.name}`, {
|
|
212
228
|
...(config.gateTimeout ? { timeout: config.gateTimeout } : {}),
|
|
213
229
|
description:
|
|
214
230
|
config.gateDescription ??
|