@intentius/chant-lexicon-terraform 0.124.27 → 0.124.28
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/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "f7180992dc1958b02021c16194c6e8708902e711ca1a94b93536c377594db42b",
|
|
5
5
|
"meta.json": "ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356",
|
|
6
6
|
"types/index.d.ts": "022927e6a7b351cc4cf52d5f973f60ccd140e5810e17b32fb8868965aa1db161",
|
|
7
7
|
"rules/plan-before-apply.ts": "7c10245b2c3335253b48ee7d8b075d89f43116bde20313a5d1031b343fef1282",
|
|
@@ -56,5 +56,5 @@
|
|
|
56
56
|
"rules/tf045.ts": "760b1f4f385fbe7a86897efe3078fdeac11bcc9bfb806285d63db50488174b7b",
|
|
57
57
|
"skills/chant-terraform.md": "d9ca7b7c28aed0038ce775a3491150298492eed2858fd3651ed9bfca0e577ae8"
|
|
58
58
|
},
|
|
59
|
-
"composite": "
|
|
59
|
+
"composite": "cef3ea2d21a047946dd260f00650538395ca7d5448da7f1b6ceefc1ce22a3212"
|
|
60
60
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-terraform",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src/",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"access": "public"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@intentius/chant": "^0.124.
|
|
70
|
+
"@intentius/chant": "^0.124.28",
|
|
71
71
|
"zod": "^4.3.6"
|
|
72
72
|
},
|
|
73
73
|
"description": "terraform lexicon for chant, existing Terraform root modules as declarable entities",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stored change set's node ids against the spawned `chant graph --format ir`
|
|
3
|
+
* of a real terraform root (#4022).
|
|
4
|
+
*
|
|
5
|
+
* The root is `__fixtures__/change-set/v2.tf.txt` with its local module
|
|
6
|
+
* `v2-module-app.tf.txt` (less the sensitive variable's default), and the plan is `terraform.plan.json`, the real
|
|
7
|
+
* `show -json` of applying v2 over v1 (`../change-set.test.ts`). The graph is
|
|
8
|
+
* read the way `components pr-apply` reads it, by starting chant. The fast
|
|
9
|
+
* pull-request loop test (`pr-loop.tofu.test.ts`) reads the same ids in
|
|
10
|
+
* process instead; the first test here holds the two equal.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
18
|
+
import { composeChangeSet } from "@intentius/chant/change-set";
|
|
19
|
+
import { buildGraphIr } from "@intentius/chant/graph-ir";
|
|
20
|
+
import { readGraphIds, storeGateChangeSet } from "@intentius/chant/lifecycle/gate-change-set";
|
|
21
|
+
import { readStoredChangeSet } from "@intentius/chant/lifecycle/change-set-store";
|
|
22
|
+
import { terraformChangeSetPart } from "../change-set";
|
|
23
|
+
import { renderTerraformRoots } from "../hcl/roots";
|
|
24
|
+
|
|
25
|
+
const REPO = join(import.meta.dirname, "..", "..", "..", "..");
|
|
26
|
+
const FIXTURES = join(import.meta.dirname, "..", "__fixtures__", "change-set");
|
|
27
|
+
const TIMEOUT = 240_000;
|
|
28
|
+
|
|
29
|
+
describe("a terraform change set mapped onto the spawned chant graph", () => {
|
|
30
|
+
let dir: string;
|
|
31
|
+
const git = (...a: string[]) => {
|
|
32
|
+
const r = spawnSync("git", a, { cwd: dir, encoding: "utf8" });
|
|
33
|
+
if (r.status !== 0) throw new Error(`git ${a.join(" ")}: ${r.stderr}`);
|
|
34
|
+
return r.stdout.trim();
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
beforeAll(() => {
|
|
38
|
+
dir = realpathSync(mkdtempSync(join(tmpdir(), "chant-tf-change-set-graph-")));
|
|
39
|
+
symlinkSync(join(REPO, "node_modules"), join(dir, "node_modules"));
|
|
40
|
+
mkdirSync(join(dir, "estate", "modules", "app"), { recursive: true });
|
|
41
|
+
// The sensitive variable's committed default is dropped: `chant graph` refuses source that fails
|
|
42
|
+
// `chant lint` (TF006, TF007), and the block, which is all the graph needs, stays.
|
|
43
|
+
writeFileSync(join(dir, "estate", "main.tf"), readFileSync(join(FIXTURES, "v2.tf.txt"), "utf8").replace(/\n\s*default\s*=\s*"s3cr3t-2"/, ""));
|
|
44
|
+
writeFileSync(join(dir, "estate", "modules", "app", "main.tf"), readFileSync(join(FIXTURES, "v2-module-app.tf.txt"), "utf8"));
|
|
45
|
+
writeFileSync(join(dir, "chant.config.json"), JSON.stringify({ lexicons: ["terraform"], terraform: { roots: { estate: { dir: "estate" } } } }));
|
|
46
|
+
writeFileSync(join(dir, ".gitignore"), "node_modules\n");
|
|
47
|
+
git("init", "-q", "-b", "main");
|
|
48
|
+
git("config", "user.email", "ci@example.test");
|
|
49
|
+
git("config", "user.name", "ci");
|
|
50
|
+
git("add", "-A");
|
|
51
|
+
git("commit", "-qm", "estate");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterAll(() => {
|
|
55
|
+
if (dir) rmSync(dir, { recursive: true, force: true });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("the in-process graph has the ids chant graph --format ir prints", async () => {
|
|
59
|
+
const spawned = await readGraphIds(dir);
|
|
60
|
+
if ("error" in spawned) throw new Error(spawned.error);
|
|
61
|
+
const rendered = await renderTerraformRoots({ projectRoot: dir, roots: { estate: { dir: "estate" } } });
|
|
62
|
+
expect(buildGraphIr(rendered.entities).nodes.map((n) => n.id).sort()).toEqual([...spawned.ids].sort());
|
|
63
|
+
}, TIMEOUT);
|
|
64
|
+
|
|
65
|
+
test("each entry is stored on the block the graph names, and a deleted block has no node", async () => {
|
|
66
|
+
const plan = JSON.parse(readFileSync(join(FIXTURES, "terraform.plan.json"), "utf8"));
|
|
67
|
+
const doc = composeChangeSet([terraformChangeSetPart({ member: "estate", plan })]);
|
|
68
|
+
const head = git("rev-parse", "HEAD");
|
|
69
|
+
const stored = await storeGateChangeSet({ doc, cwd: dir, at: head });
|
|
70
|
+
expect(stored.warning).toBeUndefined();
|
|
71
|
+
expect(stored).toMatchObject({ written: true, graph: { member: null, at: head } });
|
|
72
|
+
const read = await readStoredChangeSet(doc.digest, { cwd: dir });
|
|
73
|
+
if (read.status !== "found") throw new Error(read.status);
|
|
74
|
+
expect(read.document.entries.map((e) => [e.address, e.node])).toEqual([
|
|
75
|
+
["module.app.terraform_data.instance[0]", "estate/module.app/terraform_data.instance"],
|
|
76
|
+
["module.app.terraform_data.instance[1]", "estate/module.app/terraform_data.instance"],
|
|
77
|
+
["terraform_data.config", "estate/terraform_data.config"],
|
|
78
|
+
["terraform_data.keep", "estate/terraform_data.keep"],
|
|
79
|
+
["terraform_data.old", null],
|
|
80
|
+
["terraform_data.secret", "estate/terraform_data.secret"],
|
|
81
|
+
["terraform_data.worker", "estate/terraform_data.worker"],
|
|
82
|
+
]);
|
|
83
|
+
}, TIMEOUT);
|
|
84
|
+
});
|
|
@@ -22,7 +22,9 @@ import { dirname, join } from "node:path";
|
|
|
22
22
|
import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
|
|
23
23
|
import type { ParsedArgs } from "@intentius/chant/cli/registry";
|
|
24
24
|
import type { PrReport } from "@intentius/chant/pr-loop";
|
|
25
|
+
import { buildGraphIr } from "@intentius/chant/graph-ir";
|
|
25
26
|
import { terraformPlugin } from "../plugin";
|
|
27
|
+
import { renderTerraformRoots } from "../hcl/roots";
|
|
26
28
|
|
|
27
29
|
const REPO = join(import.meta.dirname, "..", "..", "..", "..");
|
|
28
30
|
|
|
@@ -44,10 +46,25 @@ const ROOTS: Record<string, string> = {
|
|
|
44
46
|
dns: 'resource "terraform_data" "zone" {\n input = "example.test"\n}\n',
|
|
45
47
|
};
|
|
46
48
|
|
|
49
|
+
// The source passes `chant lint`, since `chant graph --format ir`, which maps
|
|
50
|
+
// the stored change set onto nodes (#4022), refuses source that does not.
|
|
51
|
+
/**
|
|
52
|
+
* The estate's graph node ids, read in process with the pair `chant graph
|
|
53
|
+
* --format ir` runs for terraform roots (`renderTerraformRoots`, then
|
|
54
|
+
* `buildGraphIr`), so pr-apply maps its stored change set without starting a
|
|
55
|
+
* chant child process. `change-set-graph.e2e.test.ts` holds these ids equal
|
|
56
|
+
* to the spawned `chant graph --format ir`'s.
|
|
57
|
+
*/
|
|
58
|
+
async function estateGraphIds(projectRoot: string): Promise<{ ids: string[] }> {
|
|
59
|
+
const config = JSON.parse(readFileSync(join(projectRoot, "chant.config.json"), "utf8")) as { terraform: { binary?: string; roots: Record<string, { dir: string }> } };
|
|
60
|
+
const rendered = await renderTerraformRoots({ projectRoot, roots: config.terraform.roots, ...(config.terraform.binary ? { binary: config.terraform.binary } : {}) });
|
|
61
|
+
return { ids: buildGraphIr(rendered.entities).nodes.map((n) => n.id) };
|
|
62
|
+
}
|
|
63
|
+
|
|
47
64
|
const COMPONENTS = `import { phase, stackOutput, type Component } from "@intentius/chant/components";
|
|
48
65
|
|
|
49
66
|
const apply = (root: string, vars?: Record<string, unknown>) =>
|
|
50
|
-
[phase("Apply", [{ kind: "terraform-apply", root,
|
|
67
|
+
[phase("Apply", [{ kind: "terraform-apply", root, noRollback: "test roots with local state", vars: vars ?? {} }])];
|
|
51
68
|
|
|
52
69
|
export const net: Component = { name: "net", dependsOn: [], deploy: apply("net") };
|
|
53
70
|
export const a: Component = { name: "a", dependsOn: ["net"], deploy: apply("a", { cidr: stackOutput("net", "cidr") }) };
|
|
@@ -151,6 +168,7 @@ describe.skipIf(!BINARY)(`the pull-request loop over five ${BINARY ?? "terraform
|
|
|
151
168
|
});
|
|
152
169
|
vi.stubGlobal("fetch", github.fetch);
|
|
153
170
|
handlers = await import("@intentius/chant/cli/handlers/pr");
|
|
171
|
+
(await import("@intentius/chant/lifecycle/gate-change-set")).setGraphIdReader(estateGraphIds);
|
|
154
172
|
({ recordGateApproval } = await import("@intentius/chant/cli/handlers/operator"));
|
|
155
173
|
|
|
156
174
|
// Every root applied once, so each has state and outputs to read.
|
|
@@ -159,7 +177,8 @@ describe.skipIf(!BINARY)(`the pull-request loop over five ${BINARY ?? "terraform
|
|
|
159
177
|
expect(await runComponentsFanOut(ctx({ fromAffected: "all.json" }) as never)).toBe(0);
|
|
160
178
|
}, 240_000);
|
|
161
179
|
|
|
162
|
-
afterAll(() => {
|
|
180
|
+
afterAll(async () => {
|
|
181
|
+
(await import("@intentius/chant/lifecycle/gate-change-set")).setGraphIdReader(undefined);
|
|
163
182
|
vi.unstubAllGlobals();
|
|
164
183
|
if (cwd) process.chdir(cwd);
|
|
165
184
|
for (const key of Object.keys(process.env)) if (!(key in env)) delete process.env[key];
|
|
@@ -222,6 +241,25 @@ describe.skipIf(!BINARY)(`the pull-request loop over five ${BINARY ?? "terraform
|
|
|
222
241
|
expect(github.statuses.at(-1)).toMatchObject({ context: "chant/apply", state: "success", sha: applied.head });
|
|
223
242
|
}, 240_000);
|
|
224
243
|
|
|
244
|
+
test("the change set the approval bound is stored by its digest, each entry on its graph node (#4022)", async () => {
|
|
245
|
+
const applied = report("apply");
|
|
246
|
+
const { changeSetShowDocument } = await import("@intentius/chant/workspace/change-set-show");
|
|
247
|
+
const shown = await changeSetShowDocument({ cwd: dir, digest: applied.digest });
|
|
248
|
+
if ("error" in shown) throw new Error(shown.error.message);
|
|
249
|
+
expect(shown).toMatchObject({ member: null, digest: applied.digest, reason: null });
|
|
250
|
+
const stored = shown.changeSet!;
|
|
251
|
+
expect(stored.digest).toBe(applied.digest);
|
|
252
|
+
expect(stored.graph).toEqual({ member: null, at: applied.head });
|
|
253
|
+
// The plan's addresses, each on the block the terraform lexicon's graph names `<root>/<address>`.
|
|
254
|
+
expect(stored.entries.map((e) => [e.member, e.address, e.action, e.node])).toEqual([
|
|
255
|
+
["a", "terraform_data.subnet", "update", "a/terraform_data.subnet"],
|
|
256
|
+
["app", "terraform_data.app", "no-op", "app/terraform_data.app"],
|
|
257
|
+
]);
|
|
258
|
+
// And those are ids the estate's graph has.
|
|
259
|
+
const graph = await estateGraphIds(dir);
|
|
260
|
+
for (const e of stored.entries) expect(graph.ids).toContain(e.node);
|
|
261
|
+
}, 240_000);
|
|
262
|
+
|
|
225
263
|
test("a plan that changed after review is refused, naming both digests, and nothing applies", async () => {
|
|
226
264
|
const reviewed = await openPullRequest(2, "a-v3");
|
|
227
265
|
github.reviews[2] = [{ user: { login: "alice" }, state: "APPROVED" }];
|
|
@@ -240,5 +278,11 @@ describe.skipIf(!BINARY)(`the pull-request loop over five ${BINARY ?? "terraform
|
|
|
240
278
|
expect(state("a")).toContain("a-v2");
|
|
241
279
|
expect(state("a")).not.toContain("a-v4");
|
|
242
280
|
expect(github.statuses.at(-1)).toMatchObject({ context: "chant/apply", state: "failure", description: "refused: the plan changed after review" });
|
|
281
|
+
|
|
282
|
+
// The gate now waits on the plan it was refused, and workspace status links it to that change set (#4022).
|
|
283
|
+
const { readMemberGates } = await import("@intentius/chant/workspace/status-gates");
|
|
284
|
+
const { gates } = await readMemberGates("_gates", "flat", git("rev-parse", "chant/lifecycle"), [], dir, new Date().toISOString());
|
|
285
|
+
const gate = gates.find((g) => g.component === "pr-2")!;
|
|
286
|
+
expect(gate).toMatchObject({ state: "pending", planDigest: refused.digest, changeSet: refused.digest });
|
|
243
287
|
}, 240_000);
|
|
244
288
|
});
|