@intentius/chant-lexicon-terraform 0.60.0 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/composites/terraform-adopt-op.d.ts +18 -2
- package/dist/composites/terraform-adopt-op.d.ts.map +1 -1
- package/dist/describe-resources.d.ts +59 -0
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/hcl/edges.d.ts +124 -0
- package/dist/hcl/edges.d.ts.map +1 -0
- package/dist/hcl/parse.d.ts +42 -0
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/roots.d.ts +6 -0
- package/dist/hcl/roots.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/terraform.d.ts +44 -8
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/adoption.d.ts +110 -35
- package/dist/op/adoption.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__fixtures__/ACCEPTANCE.md +135 -60
- package/src/__fixtures__/graph-roots/README.md +21 -0
- package/src/__fixtures__/graph-roots/app/main.tf +41 -0
- package/src/__fixtures__/graph-roots/app/modules/cdn/main.tf +7 -0
- package/src/__fixtures__/graph-roots/network/main.tf +16 -0
- package/src/__fixtures__/live-estate/README.md +65 -19
- package/src/__fixtures__/live-estate/adoptable.tf +13 -0
- package/src/__fixtures__/live-ls.json +5 -5
- package/src/__fixtures__/live-plan.json +67 -35
- package/src/composites/terraform-adopt-op.acceptance.test.ts +101 -50
- package/src/composites/terraform-adopt-op.test.ts +33 -11
- package/src/composites/terraform-adopt-op.ts +20 -2
- package/src/composites/terraform-apply-op.acceptance.test.ts +4 -2
- package/src/describe-resources.live.test.ts +76 -4
- package/src/describe-resources.test.ts +35 -0
- package/src/describe-resources.ts +110 -3
- package/src/hcl/edges.test.ts +207 -0
- package/src/hcl/edges.ts +316 -0
- package/src/hcl/parse.ts +53 -0
- package/src/hcl/roots.ts +14 -0
- package/src/index.ts +1 -0
- package/src/op/activities/choudoufu.test.ts +3 -3
- package/src/op/activities/terraform.ts +70 -21
- package/src/op/adoption.test.ts +129 -11
- package/src/op/adoption.ts +181 -49
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* `terraform-adopt-op.test.ts` next door proves the shape chant emits.
|
|
6
6
|
* `choudoufu.test.ts` proves the activities' contract against a stubbed child
|
|
7
7
|
* process. This proves the shape does the thing: create an unmarked VPC
|
|
8
|
-
* directly against choudoufu's pinned emulator
|
|
9
|
-
* estate does not own
|
|
10
|
-
* the Ledger step and expect exactly one
|
|
11
|
-
* values on it; run the Adopt step and let
|
|
12
|
-
* the estate to own the same VPC, with
|
|
8
|
+
* directly against choudoufu's pinned emulator at the cidr this root declares,
|
|
9
|
+
* so it is a live resource this estate does not own and the sweep can match to
|
|
10
|
+
* a declaration by content; run the Ledger step and expect exactly one
|
|
11
|
+
* adoptable match with the two marker values on it; run the Adopt step and let
|
|
12
|
+
* it write them; re-plan and expect the estate to own the same VPC, with
|
|
13
|
+
* nothing left adoptable.
|
|
13
14
|
*
|
|
14
15
|
* The Op's phases are not run through `runOpLocally` here, deliberately.
|
|
15
16
|
* `TerraformAdoptOp` always emits a gate — adoption moves the estate's
|
|
@@ -20,38 +21,39 @@
|
|
|
20
21
|
* hand-off, which is the part a real binary can falsify. `choudoufu.acceptance.test.ts` takes the same approach for the same
|
|
21
22
|
* reason.
|
|
22
23
|
*
|
|
23
|
-
* ##
|
|
24
|
+
* ## What this suite needed, and when it arrived
|
|
24
25
|
*
|
|
25
|
-
*
|
|
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.
|
|
26
|
+
* It had never passed on any binary until choudoufu v0.15.0.
|
|
30
27
|
*
|
|
31
28
|
* The document's `unowned[]` is the resources found at an identity the
|
|
32
|
-
* configuration itself declares.
|
|
33
|
-
* name is in the block), so an unmarked live one comes back
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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": []`.
|
|
29
|
+
* configuration itself declares. An `aws_cloudwatch_log_group` has one (its
|
|
30
|
+
* name is in the block), so an unmarked live one comes back there with
|
|
31
|
+
* `adopt_tofu_estate`/`adopt_tofu_address` on it. An `aws_vpc` has none: EC2
|
|
32
|
+
* assigns the id, so the document reports `omissions[].reason =
|
|
33
|
+
* "NEEDS_DISCOVERY"` and `unowned[]` stays empty. The live VPC is matched
|
|
34
|
+
* instead by choudoufu's content matcher during the estate-wide sweep, and
|
|
35
|
+
* until v0.15.0 that match was printed only in the human `-adoption-only`
|
|
36
|
+
* render, which choudoufu refuses alongside `-json`. chant #2168 ran this
|
|
37
|
+
* block against v0.14.0 on 2026-09-07, reached the document and stopped, with
|
|
38
|
+
* `ledger.adoptions` empty, and filed
|
|
39
|
+
* [choudoufu #962](https://github.com/INTENTIUS/choudoufu/issues/962).
|
|
46
40
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
41
|
+
* choudoufu PR 963 answered it: the document now carries the match as an
|
|
42
|
+
* `adoptable[]` section, with `swept[]` beside it, and each row carries the
|
|
43
|
+
* two marker values, the arguments the match rested on, and the tagging
|
|
44
|
+
* command. `choudoufuLivePlan` puts `TOFU_LIVE_COLLECT_UNCLAIMED=1` on the
|
|
45
|
+
* `-json` run under `adoptionOnly` so the section is populated at all
|
|
46
|
+
* (choudoufu's `-json` run asks no sweep of its own), and
|
|
47
|
+
* `readAdoptionLedger` reads both sections. Which is what this suite exists
|
|
48
|
+
* to falsify, so the gate that named #962 is gone and only the three ordinary
|
|
49
|
+
* dependencies remain.
|
|
52
50
|
*
|
|
53
|
-
* The
|
|
54
|
-
*
|
|
51
|
+
* The fixture stays an `aws_vpc` deliberately. A log group would make the
|
|
52
|
+
* block pass off `unowned[]` alone and would stop it proving the
|
|
53
|
+
* content-matcher path, which is the only thing it exists to prove.
|
|
54
|
+
*
|
|
55
|
+
* The gate below is those three: a `choudoufu`, an `aws` CLI, and the
|
|
56
|
+
* emulator's endpoint.
|
|
55
57
|
*
|
|
56
58
|
* Gating copied from `./terraform-apply-op.acceptance.test.ts` (`onPath`),
|
|
57
59
|
* which in turn copies `lexicons/k3s/src/serializer.acceptance.test.ts`'s
|
|
@@ -59,7 +61,7 @@
|
|
|
59
61
|
* failing when the real dependency is absent.
|
|
60
62
|
*
|
|
61
63
|
* `../__fixtures__/ACCEPTANCE.md` records what this block has last passed
|
|
62
|
-
* against,
|
|
64
|
+
* against, and the whole history of why it could not before.
|
|
63
65
|
*/
|
|
64
66
|
|
|
65
67
|
import { execSync } from "node:child_process";
|
|
@@ -67,6 +69,8 @@ import { cpSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
67
69
|
import { tmpdir } from "node:os";
|
|
68
70
|
import { join } from "node:path";
|
|
69
71
|
import { afterAll, describe, expect, it } from "vitest";
|
|
72
|
+
import { loadActivities, loadProfiles, runOpLocally, type OpConfig } from "@intentius/chant/op";
|
|
73
|
+
import { TerraformAdoptOp } from "./terraform-adopt-op";
|
|
70
74
|
import { choudoufuAdopt, choudoufuLivePlan, terraformInit } from "../op/activities/terraform";
|
|
71
75
|
|
|
72
76
|
function onPath(cmd: string): boolean {
|
|
@@ -78,18 +82,6 @@ function onPath(cmd: string): boolean {
|
|
|
78
82
|
}
|
|
79
83
|
}
|
|
80
84
|
|
|
81
|
-
/**
|
|
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.
|
|
90
|
-
*/
|
|
91
|
-
const CHOUDOUFU_ADOPTABLE_NOT_IN_DOCUMENT = true;
|
|
92
|
-
|
|
93
85
|
const emulatorEndpoint = process.env.CHOUDOUFU_EMULATOR_ENDPOINT;
|
|
94
86
|
|
|
95
87
|
const skipReason = !onPath("choudoufu")
|
|
@@ -98,11 +90,7 @@ const skipReason = !onPath("choudoufu")
|
|
|
98
90
|
? "no aws CLI on PATH (the unmarked resource is created with it, and adopted through it)"
|
|
99
91
|
: !emulatorEndpoint
|
|
100
92
|
? "CHOUDOUFU_EMULATOR_ENDPOINT is not set (bring up choudoufu's `just smoke` emulator stack and export it)"
|
|
101
|
-
:
|
|
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"
|
|
105
|
-
: "";
|
|
93
|
+
: "";
|
|
106
94
|
|
|
107
95
|
const FIXTURE = join(import.meta.dirname, "..", "__fixtures__", "live-adopt");
|
|
108
96
|
const ESTATE = "chant-adopt-fixture";
|
|
@@ -191,3 +179,66 @@ describe.skipIf(skipReason !== "")(
|
|
|
191
179
|
);
|
|
192
180
|
},
|
|
193
181
|
);
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* #2302: `TerraformAdoptOp` built with no Init phase reached its Ledger step
|
|
185
|
+
* with an empty `.terraform` on every checkout that had never run `init` —
|
|
186
|
+
* every fresh CI checkout, not a GitLab peculiarity. This drives the built Op
|
|
187
|
+
* itself through `runOpLocally`, the way `terraform-apply-op.acceptance.test.ts`
|
|
188
|
+
* does, over a project this test creates fresh and never initializes by hand:
|
|
189
|
+
* no `terraformInit` call anywhere in this block, unlike the suite above. The
|
|
190
|
+
* Op's own Init phase is what has to do that work, or the Ledger step fails
|
|
191
|
+
* the same way it failed in INTENTIUS/choudoufu#1026.
|
|
192
|
+
*
|
|
193
|
+
* `TerraformAdoptOp` gates unconditionally (no `gate: "never"`, unlike
|
|
194
|
+
* `TerraformApplyOp`), so a passing run here ends with `status: "gated"`
|
|
195
|
+
* right after the Ledger phase — "reaching its gate" is the pass, not a
|
|
196
|
+
* completed adoption. That is proof enough: the Ledger step needs the
|
|
197
|
+
* provider schema, and a run that reaches the gate is a run whose Ledger step
|
|
198
|
+
* already read it.
|
|
199
|
+
*
|
|
200
|
+
* Needs only a `choudoufu` on PATH and the emulator, not the `aws` CLI: this
|
|
201
|
+
* block creates no live resource and never reads `ledger.adoptions`, so a
|
|
202
|
+
* narrower gate than the suite above is correct instead of borrowed.
|
|
203
|
+
*/
|
|
204
|
+
const gateSkipReason = !onPath("choudoufu")
|
|
205
|
+
? "no choudoufu binary on PATH"
|
|
206
|
+
: !emulatorEndpoint
|
|
207
|
+
? "CHOUDOUFU_EMULATOR_ENDPOINT is not set (bring up choudoufu's `just smoke` emulator stack and export it)"
|
|
208
|
+
: "";
|
|
209
|
+
|
|
210
|
+
describe.skipIf(gateSkipReason !== "")(
|
|
211
|
+
`TerraformAdoptOp reaches its gate on a checkout that has never run init${gateSkipReason ? ` (skipped: ${gateSkipReason})` : ""}`,
|
|
212
|
+
() => {
|
|
213
|
+
it(
|
|
214
|
+
"Init downloads the provider the Ledger step needs, and the run reaches its gate",
|
|
215
|
+
{ timeout: 300_000 },
|
|
216
|
+
async () => {
|
|
217
|
+
const dir = project();
|
|
218
|
+
|
|
219
|
+
process.env.AWS_ENDPOINT_URL ??= emulatorEndpoint;
|
|
220
|
+
process.env.AWS_ACCESS_KEY_ID ??= "choudoufu-emulator";
|
|
221
|
+
process.env.AWS_SECRET_ACCESS_KEY ??= "choudoufu-emulator";
|
|
222
|
+
process.env.AWS_DEFAULT_REGION ??= "us-east-1";
|
|
223
|
+
|
|
224
|
+
// No `terraformInit` call here. `dir/root` is a fresh checkout: no
|
|
225
|
+
// `.terraform`, only the fixture's own `main.tf` and
|
|
226
|
+
// `estate.chdf.hcl` — exactly what `git clone` leaves behind, since
|
|
227
|
+
// `.terraform` is what every `.gitignore` in this ecosystem excludes.
|
|
228
|
+
const { op } = TerraformAdoptOp({ name: "gate-check-adopt", root: "estate", cwd: dir });
|
|
229
|
+
|
|
230
|
+
const activities = await loadActivities(["terraform"]);
|
|
231
|
+
const result = await runOpLocally((op as unknown as { props: OpConfig }).props, activities, await loadProfiles());
|
|
232
|
+
|
|
233
|
+
expect(result.status).toBe("gated");
|
|
234
|
+
expect(result.records.map((r) => `${r.phase}:${r.status}`)).toEqual([
|
|
235
|
+
"Init:ok",
|
|
236
|
+
"Check:ok",
|
|
237
|
+
"Ledger:ok",
|
|
238
|
+
"Gate:skipped",
|
|
239
|
+
"Adopt:skipped",
|
|
240
|
+
]);
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
},
|
|
244
|
+
);
|
|
@@ -26,15 +26,37 @@ const isActivity = (s: StepDefinition): s is ActivityStep => s.kind === "activit
|
|
|
26
26
|
const steps = (op: OpConfig): ActivityStep[] => op.phases.flatMap((p) => p.steps.filter(isActivity));
|
|
27
27
|
const base = { name: "estate-adopt", root: "estate" };
|
|
28
28
|
|
|
29
|
-
describe("TerraformAdoptOp phases (#2105)", () => {
|
|
30
|
-
test("Check, Ledger, Gate, Adopt, in that order", () => {
|
|
29
|
+
describe("TerraformAdoptOp phases (#2105, #2302)", () => {
|
|
30
|
+
test("Init, Check, Ledger, Gate, Adopt, in that order", () => {
|
|
31
31
|
const op = props(base);
|
|
32
|
-
expect(phaseNames(op)).toEqual(["Check", "Ledger", "Gate", "Adopt"]);
|
|
33
|
-
expect(steps(op).map((s) => s.fn)).toEqual([
|
|
32
|
+
expect(phaseNames(op)).toEqual(["Init", "Check", "Ledger", "Gate", "Adopt"]);
|
|
33
|
+
expect(steps(op).map((s) => s.fn)).toEqual([
|
|
34
|
+
"terraformInit",
|
|
35
|
+
"choudoufuLiveCheck",
|
|
36
|
+
"choudoufuLivePlan",
|
|
37
|
+
"choudoufuAdopt",
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// #2302: the Ledger step's `live-plan` needs the provider schema to
|
|
42
|
+
// discover markers, which a checkout that has never run `init` does not
|
|
43
|
+
// have — every fresh CI checkout, not a GitLab peculiarity. Without this
|
|
44
|
+
// phase `live-plan` fails outright: "Error: Provider unavailable for
|
|
45
|
+
// marker discovery".
|
|
46
|
+
test("Init runs terraform init first, the same shape TerraformApplyOp and TerraformWatchOp use", () => {
|
|
47
|
+
const init = props(base).phases[0].steps[0] as ActivityStep;
|
|
48
|
+
expect(init.fn).toBe("terraformInit");
|
|
49
|
+
expect(init.args?.root).toBe("estate");
|
|
50
|
+
expect(init.args && "upgrade" in init.args).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("upgrade rides the Init step only, the same option name as the other two Ops", () => {
|
|
54
|
+
const init = props({ ...base, upgrade: true }).phases[0].steps[0] as ActivityStep;
|
|
55
|
+
expect(init.args?.upgrade).toBe(true);
|
|
34
56
|
});
|
|
35
57
|
|
|
36
|
-
test("Check runs live-check
|
|
37
|
-
const check = props(base).phases[
|
|
58
|
+
test("Check runs live-check second, and reports whether the root was refused", () => {
|
|
59
|
+
const check = props(base).phases[1].steps[0] as ActivityStep;
|
|
38
60
|
expect(check.fn).toBe("choudoufuLiveCheck");
|
|
39
61
|
expect(check.outcomeAttribute).toEqual({ name: "Refused", from: "refused" });
|
|
40
62
|
// No cloud calls, so this is the cheapest place to learn the root cannot
|
|
@@ -43,7 +65,7 @@ describe("TerraformAdoptOp phases (#2105)", () => {
|
|
|
43
65
|
});
|
|
44
66
|
|
|
45
67
|
test("Ledger is the -adoption-only live plan, and publishes both counts", () => {
|
|
46
|
-
const ledger = props(base).phases[
|
|
68
|
+
const ledger = props(base).phases[2].steps[0] as ActivityStep;
|
|
47
69
|
expect(ledger.fn).toBe("choudoufuLivePlan");
|
|
48
70
|
expect(ledger.id).toBe("ledger");
|
|
49
71
|
expect(ledger.args?.adoptionOnly).toBe(true);
|
|
@@ -64,14 +86,14 @@ describe("TerraformAdoptOp phases (#2105)", () => {
|
|
|
64
86
|
|
|
65
87
|
test("an explicit estate rides the Ledger step only", () => {
|
|
66
88
|
const op = props({ ...base, estate: "prod-networking" });
|
|
67
|
-
expect((op.phases[
|
|
68
|
-
expect((op.phases[
|
|
89
|
+
expect((op.phases[2].steps[0] as ActivityStep).args?.estate).toBe("prod-networking");
|
|
90
|
+
expect((op.phases[1].steps[0] as ActivityStep).args?.estate).toBeUndefined();
|
|
69
91
|
});
|
|
70
92
|
});
|
|
71
93
|
|
|
72
94
|
describe("TerraformAdoptOp gates on the ledger (#2105)", () => {
|
|
73
95
|
const gateOf = (op: OpConfig): GateStep => {
|
|
74
|
-
const step = op.phases[
|
|
96
|
+
const step = op.phases[3].steps[0];
|
|
75
97
|
expect(step.kind).toBe("gate");
|
|
76
98
|
return step as GateStep;
|
|
77
99
|
};
|
|
@@ -107,7 +129,7 @@ describe("TerraformAdoptOp gates on the ledger (#2105)", () => {
|
|
|
107
129
|
});
|
|
108
130
|
|
|
109
131
|
describe("TerraformAdoptOp adopts the ledger's matches and nothing else (#2105)", () => {
|
|
110
|
-
const adoptStep = (op: OpConfig): ActivityStep => op.phases[
|
|
132
|
+
const adoptStep = (op: OpConfig): ActivityStep => op.phases[4].steps[0] as ActivityStep;
|
|
111
133
|
|
|
112
134
|
test("the Adopt step takes the ledger's adoptable set by reference", () => {
|
|
113
135
|
const adopt = adoptStep(props(base));
|
|
@@ -13,8 +13,18 @@
|
|
|
13
13
|
* the two values that would claim it. Cloud-to-code here is "claim what the
|
|
14
14
|
* configuration already describes" — a tag write, not a regeneration.
|
|
15
15
|
*
|
|
16
|
-
* Phases: Check, Ledger, Gate, Adopt.
|
|
16
|
+
* Phases: Init, Check, Ledger, Gate, Adopt.
|
|
17
17
|
*
|
|
18
|
+
* - **Init** runs `terraform init`, the same step `TerraformApplyOp` and
|
|
19
|
+
* `TerraformWatchOp` open with. The Ledger step's `live-plan` needs the
|
|
20
|
+
* provider schema to discover markers on the live resources it reads, and
|
|
21
|
+
* a checkout that has never run `init` has no `.terraform` directory to
|
|
22
|
+
* read one from — which is every fresh CI checkout, not a GitLab
|
|
23
|
+
* peculiarity (#2302). Without this phase `live-plan` fails outright:
|
|
24
|
+
* `Error: Provider unavailable for marker discovery`. This composite had
|
|
25
|
+
* no Init phase because nothing had ever run `live-adopt` before
|
|
26
|
+
* INTENTIUS/choudoufu#1026 did, so the gap survived every guard that only
|
|
27
|
+
* read the generated YAML.
|
|
18
28
|
* - **Check** runs `live-check`, which makes no cloud calls and says whether
|
|
19
29
|
* this configuration can move under markers at all. It runs first because
|
|
20
30
|
* a root that `live-check` refuses will produce an adoption ledger that
|
|
@@ -83,6 +93,7 @@
|
|
|
83
93
|
|
|
84
94
|
import { Op, phase, gate, activity, OpResource } from "@intentius/chant/op";
|
|
85
95
|
import {
|
|
96
|
+
terraformInit as initStep,
|
|
86
97
|
choudoufuLiveCheck as checkStep,
|
|
87
98
|
choudoufuLivePlan as ledgerStep,
|
|
88
99
|
choudoufuAdopt as adoptStep,
|
|
@@ -110,6 +121,12 @@ export interface TerraformAdoptOpConfig {
|
|
|
110
121
|
gateTimeout?: string;
|
|
111
122
|
/** Override the gate description shown to the approver. */
|
|
112
123
|
gateDescription?: string;
|
|
124
|
+
/**
|
|
125
|
+
* `-upgrade` on the Init step: re-resolve provider and module versions.
|
|
126
|
+
* Same field, same meaning, as `TerraformApplyOpConfig.upgrade` and
|
|
127
|
+
* `TerraformWatchOpConfig.upgrade`.
|
|
128
|
+
*/
|
|
129
|
+
upgrade?: boolean;
|
|
113
130
|
/**
|
|
114
131
|
* Directory each step starts the `chant.config.*` search from, which is what
|
|
115
132
|
* `terraform.roots` and the root's relative `dir` resolve against. Default:
|
|
@@ -130,7 +147,7 @@ export interface TerraformAdoptOpConfig {
|
|
|
130
147
|
}
|
|
131
148
|
|
|
132
149
|
export interface TerraformAdoptOpResources {
|
|
133
|
-
/** Op resource. Generates the Check/Ledger/Gate/Adopt workflow. */
|
|
150
|
+
/** Op resource. Generates the Init/Check/Ledger/Gate/Adopt workflow. */
|
|
134
151
|
op: InstanceType<typeof OpResource>;
|
|
135
152
|
}
|
|
136
153
|
|
|
@@ -178,6 +195,7 @@ export function TerraformAdoptOp(config: TerraformAdoptOpConfig): TerraformAdopt
|
|
|
178
195
|
TerraformMode: "live",
|
|
179
196
|
},
|
|
180
197
|
phases: [
|
|
198
|
+
phase("Init", [initStep(config.root, { ...where, ...(config.upgrade ? { upgrade: true } : {}) })]),
|
|
181
199
|
phase("Check", [check]),
|
|
182
200
|
phase("Ledger", [ledger]),
|
|
183
201
|
phase("Gate", [
|
|
@@ -160,9 +160,11 @@ describe.skipIf(skipReason !== "")(
|
|
|
160
160
|
* the approval artifact
|
|
161
161
|
* ([choudoufu #878](https://github.com/INTENTIUS/choudoufu/issues/878),
|
|
162
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
|
|
163
|
+
* this Op could not be built the way it is built now; v0.14.0 moved
|
|
164
164
|
* the floor again for the document the other two Ops read
|
|
165
165
|
* ([choudoufu #894](https://github.com/INTENTIUS/choudoufu/issues/894)),
|
|
166
|
+
* and v0.15.0 moved it once more for that document's adoptable section
|
|
167
|
+
* ([choudoufu #962](https://github.com/INTENTIUS/choudoufu/issues/962)),
|
|
166
168
|
* - `CHOUDOUFU_EMULATOR_ENDPOINT` unset (bring up choudoufu's `just smoke`
|
|
167
169
|
* docker compose stack and export `http://localhost:<mapped port>`).
|
|
168
170
|
*
|
|
@@ -218,7 +220,7 @@ const liveSkipReason: string = !onPath("choudoufu")
|
|
|
218
220
|
: choudoufuVersion === undefined
|
|
219
221
|
? "the choudoufu on PATH reports no release version (a dev build), so the approval artifact cannot be assumed"
|
|
220
222
|
: isOlderVersion(choudoufuVersion, 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)`
|
|
223
|
+
? `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, #962's adoptable section in v0.15.0)`
|
|
222
224
|
: !emulatorEndpoint
|
|
223
225
|
? "CHOUDOUFU_EMULATOR_ENDPOINT is not set (bring up choudoufu's `just smoke` emulator stack and export it)"
|
|
224
226
|
: "";
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
*
|
|
9
9
|
* The live side is real too. `src/__fixtures__/live-plan.json` and
|
|
10
10
|
* `src/__fixtures__/live-ls.json` were recorded from that same configuration
|
|
11
|
-
* by
|
|
12
|
-
* floci emulator; `src/__fixtures__/live-estate/README.md` is the
|
|
13
|
-
* log, including
|
|
11
|
+
* by the choudoufu v0.15.0 release binary, running against choudoufu's own
|
|
12
|
+
* pinned floci emulator; `src/__fixtures__/live-estate/README.md` is the
|
|
13
|
+
* recording log, including what the recording still cannot do as a chant live
|
|
14
|
+
* root would and what the v0.15.0 re-recording (#2241) changed.
|
|
14
15
|
*
|
|
15
16
|
* Nothing here runs choudoufu: the activities are injected, and the stock
|
|
16
17
|
* `show` activity throws if the reader ever reaches for it.
|
|
@@ -102,6 +103,10 @@ function deps(overrides?: Partial<TerraformReadDeps>): TerraformReadDeps {
|
|
|
102
103
|
adoptions: [],
|
|
103
104
|
contested: [],
|
|
104
105
|
ambiguous: 0,
|
|
106
|
+
// The sweep's type list (#2241), likewise a projection this reader never
|
|
107
|
+
// touches: `describeResources` asks for no sweep, so a real document off
|
|
108
|
+
// this path carries an empty one too.
|
|
109
|
+
swept: [],
|
|
105
110
|
// Plan change counts (#2106), likewise unread here.
|
|
106
111
|
adds: 0,
|
|
107
112
|
changes: 0,
|
|
@@ -154,6 +159,20 @@ describe("the live fixture root is what buildRoots() calls live (#2103)", () =>
|
|
|
154
159
|
describe("indexLivePlan (#2104)", () => {
|
|
155
160
|
const index = indexLivePlan(PLAN);
|
|
156
161
|
|
|
162
|
+
it("indexes the adoptable section and the sweep's type list", () => {
|
|
163
|
+
expect([...index.adoptable.keys()]).toEqual(["aws_vpc.adoptable"]);
|
|
164
|
+
expect(index.adoptable.get("aws_vpc.adoptable")).toMatchObject({
|
|
165
|
+
type: "aws_vpc",
|
|
166
|
+
adoptEstate: ESTATE,
|
|
167
|
+
adoptAddress: "aws_vpc.adoptable",
|
|
168
|
+
matched: [{ attribute: "cidr_block", value: "10.88.0.0/16" }],
|
|
169
|
+
});
|
|
170
|
+
// The types the sweep listed in full. An empty `adoptable` is read against
|
|
171
|
+
// this: nothing found, or nothing asked.
|
|
172
|
+
expect(index.swept).toContain("aws_vpc");
|
|
173
|
+
expect(indexLivePlan({}).swept).toEqual([]);
|
|
174
|
+
});
|
|
175
|
+
|
|
157
176
|
it("indexes every section of the recorded document by address", () => {
|
|
158
177
|
expect(index.estate).toBe(ESTATE);
|
|
159
178
|
expect([...index.bound.keys()].sort()).toEqual([
|
|
@@ -215,7 +234,7 @@ describe("terraform describeResources on a live root (#2104)", () => {
|
|
|
215
234
|
const { resources } = normalizeObservation(await describeResources(await options(), deps()));
|
|
216
235
|
expect(resources["estate/aws_vpc.main"]).toMatchObject({
|
|
217
236
|
type: "Terraform::Resource",
|
|
218
|
-
physicalId: "vpc-
|
|
237
|
+
physicalId: "vpc-dc8685c5",
|
|
219
238
|
status: "bound",
|
|
220
239
|
ownership: "owned",
|
|
221
240
|
marker: { stack: ESTATE },
|
|
@@ -260,6 +279,23 @@ describe("terraform describeResources on a live root (#2104)", () => {
|
|
|
260
279
|
expect(adoptable.marker).toBeUndefined();
|
|
261
280
|
});
|
|
262
281
|
|
|
282
|
+
it("reports a content-matched VPC adoptable, with the cidr the sweep matched on", async () => {
|
|
283
|
+
// The declaration carries no identity at all (EC2 assigns a VPC id), so
|
|
284
|
+
// the document's omission for it says NEEDS_DISCOVERY, which on its own
|
|
285
|
+
// reads as unsupported-kind. The `adoptable[]` section is what turns that
|
|
286
|
+
// into a real verdict (choudoufu #962, chant #2241).
|
|
287
|
+
const { resources } = normalizeObservation(await describeResources(await options(), deps()));
|
|
288
|
+
const vpc = resources["estate/aws_vpc.adoptable"];
|
|
289
|
+
expect(vpc).toMatchObject({ status: "adoptable", ownership: "unknown" });
|
|
290
|
+
expect(vpc.physicalId).toMatch(/^vpc-/);
|
|
291
|
+
expect(vpc.attributes).toMatchObject({
|
|
292
|
+
adoptTofuEstate: ESTATE,
|
|
293
|
+
adoptTofuAddress: "aws_vpc.adoptable",
|
|
294
|
+
matchedOn: ["cidr_block=10.88.0.0/16"],
|
|
295
|
+
});
|
|
296
|
+
expect(vpc.marker).toBeUndefined();
|
|
297
|
+
});
|
|
298
|
+
|
|
263
299
|
it("reports an unowned resource at a declared identity foreign, naming who holds it", async () => {
|
|
264
300
|
const { resources } = normalizeObservation(await describeResources(await options(), deps()));
|
|
265
301
|
const held = resources["estate/aws_cloudwatch_log_group.held_elsewhere"];
|
|
@@ -310,6 +346,42 @@ describe("terraform describeResources on a live root (#2104)", () => {
|
|
|
310
346
|
});
|
|
311
347
|
});
|
|
312
348
|
|
|
349
|
+
describe("which read answered, on a live root (#2267)", () => {
|
|
350
|
+
it("says `live` for every declared entity of the root, whatever the verdict was", async () => {
|
|
351
|
+
// The sibling assertion on a stock root is in `./describe-resources.test.ts`.
|
|
352
|
+
// Together they are the whole of the claim: a renderer joining observations
|
|
353
|
+
// to nodes can tell a read of the account from a read of a state file, and
|
|
354
|
+
// so never paints the second one as drift.
|
|
355
|
+
const opts = await options();
|
|
356
|
+
const observed = normalizeObservation(await describeResources(opts, deps()));
|
|
357
|
+
expect(Object.keys(observed.sources).sort()).toEqual([...opts.entityNames].sort());
|
|
358
|
+
for (const value of Object.values(observed.sources)) expect(value).toBe("live");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("still says `live` when live-plan failed", async () => {
|
|
362
|
+
const opts = await options();
|
|
363
|
+
const observed = normalizeObservation(
|
|
364
|
+
await describeResources(
|
|
365
|
+
opts,
|
|
366
|
+
deps({
|
|
367
|
+
livePlan: (async () => {
|
|
368
|
+
throw new Error("no valid credential sources found");
|
|
369
|
+
}) as TerraformReadDeps["livePlan"],
|
|
370
|
+
}),
|
|
371
|
+
),
|
|
372
|
+
);
|
|
373
|
+
for (const value of Object.values(observed.sources)) expect(value).toBe("live");
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("is the same value the declared entity already carries on props.mode", async () => {
|
|
377
|
+
const opts = await options();
|
|
378
|
+
const { sources } = normalizeObservation(await describeResources(opts, deps()));
|
|
379
|
+
for (const [name, entity] of opts.entities) {
|
|
380
|
+
expect(sources[name]).toBe((entity.props as { mode?: string }).mode);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
313
385
|
describe("terraform describeResources live-root failures (#2104)", () => {
|
|
314
386
|
it("reports every declared entity of the root not-observed, never absent", async () => {
|
|
315
387
|
const opts = await options();
|
|
@@ -264,6 +264,41 @@ describe("terraform describeResources (#2087)", () => {
|
|
|
264
264
|
});
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
+
describe("which read answered, on a stock root (#2267)", () => {
|
|
268
|
+
it("says `state` for every declared entity of the root, whatever the verdict was", async () => {
|
|
269
|
+
const opts = await options();
|
|
270
|
+
const observed = normalizeObservation(await describeResources(opts, deps()));
|
|
271
|
+
// Present, absent and unsupported-kind all appear in this fixture, and the
|
|
272
|
+
// claim is about the read that was attempted, not about what it found.
|
|
273
|
+
expect(Object.keys(observed.sources).sort()).toEqual([...opts.entityNames].sort());
|
|
274
|
+
for (const value of Object.values(observed.sources)) expect(value).toBe("state");
|
|
275
|
+
// `null_resource.third` is declared and not in state, so it is OBSERVED-ABSENT:
|
|
276
|
+
// in neither map, and this is the only place it can say how it was read.
|
|
277
|
+
expect(observed.resources["app/null_resource.third"]).toBeUndefined();
|
|
278
|
+
expect(observed.unobserved["app/null_resource.third"]).toBeUndefined();
|
|
279
|
+
expect(observed.sources["app/null_resource.third"]).toBe("state");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("still says `state` when the read failed, since a failure is a state read that failed", async () => {
|
|
283
|
+
const opts = await options();
|
|
284
|
+
const observed = normalizeObservation(
|
|
285
|
+
await describeResources(opts, deps({ show: failing("Error acquiring the state lock") })),
|
|
286
|
+
);
|
|
287
|
+
expect(Object.keys(observed.sources).sort()).toEqual([...opts.entityNames].sort());
|
|
288
|
+
for (const value of Object.values(observed.sources)) expect(value).toBe("state");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("is the same value the declared entity already carries on props.mode", async () => {
|
|
292
|
+
// The documented join key (#2267): one fact decides both, so a consumer
|
|
293
|
+
// holding the declared graph and no observation can label from `mode`.
|
|
294
|
+
const opts = await options();
|
|
295
|
+
const { sources } = normalizeObservation(await describeResources(opts, deps()));
|
|
296
|
+
for (const [name, entity] of opts.entities) {
|
|
297
|
+
expect(sources[name]).toBe((entity.props as { mode?: string }).mode);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
267
302
|
describe("terraform describeResources failed reads (#2087)", () => {
|
|
268
303
|
it("a failed show reports every declared entity of that root read-failed, naming the root", async () => {
|
|
269
304
|
const opts = await options();
|