@intentius/chant-lexicon-terraform 0.102.0 → 0.104.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/__fixtures__/terragrunt/affected-cases.d.ts +23 -0
- package/dist/__fixtures__/terragrunt/affected-cases.d.ts.map +1 -0
- package/dist/__fixtures__/terragrunt/record.d.ts +17 -0
- package/dist/__fixtures__/terragrunt/record.d.ts.map +1 -0
- package/dist/change-set.d.ts +67 -0
- package/dist/change-set.d.ts.map +1 -0
- package/dist/changed-roots.d.ts +30 -0
- package/dist/changed-roots.d.ts.map +1 -0
- package/dist/commands.d.ts +26 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/components/capability-plugin.d.ts +54 -0
- package/dist/components/capability-plugin.d.ts.map +1 -0
- package/dist/composites/terraform-pin-rollout-op.d.ts +44 -0
- package/dist/composites/terraform-pin-rollout-op.d.ts.map +1 -0
- package/dist/hcl/descend.d.ts +1 -1
- package/dist/hcl/descend.d.ts.map +1 -1
- package/dist/hcl/edges.d.ts +2 -2
- package/dist/hcl/edges.d.ts.map +1 -1
- package/dist/hcl/parse.d.ts +29 -1
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/roots.d.ts +1 -1
- package/dist/hcl/roots.d.ts.map +1 -1
- package/dist/hcl/suppressions.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +13 -4
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/module-source.d.ts +27 -1
- package/dist/lint/post-synth/module-source.d.ts.map +1 -1
- package/dist/lint/post-synth/terragrunt.d.ts +37 -0
- package/dist/lint/post-synth/terragrunt.d.ts.map +1 -0
- package/dist/lint/post-synth/tf002.d.ts +3 -1
- package/dist/lint/post-synth/tf002.d.ts.map +1 -1
- package/dist/lint/post-synth/tf038.d.ts +22 -0
- package/dist/lint/post-synth/tf038.d.ts.map +1 -0
- package/dist/lint/post-synth/tf039.d.ts +23 -0
- package/dist/lint/post-synth/tf039.d.ts.map +1 -0
- package/dist/lint/post-synth/tf040.d.ts +30 -0
- package/dist/lint/post-synth/tf040.d.ts.map +1 -0
- package/dist/lint/post-synth/tf041.d.ts +20 -0
- package/dist/lint/post-synth/tf041.d.ts.map +1 -0
- package/dist/lint/post-synth/tf042.d.ts +18 -0
- package/dist/lint/post-synth/tf042.d.ts.map +1 -0
- package/dist/lint/post-synth/tf043.d.ts +17 -0
- package/dist/lint/post-synth/tf043.d.ts.map +1 -0
- package/dist/lint/post-synth/tf044.d.ts +17 -0
- package/dist/lint/post-synth/tf044.d.ts.map +1 -0
- package/dist/lint/post-synth/tf045.d.ts +22 -0
- package/dist/lint/post-synth/tf045.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +8 -0
- package/dist/okf/rules/TF038.md +11 -0
- package/dist/okf/rules/TF039.md +11 -0
- package/dist/okf/rules/TF040.md +11 -0
- package/dist/okf/rules/TF041.md +11 -0
- package/dist/okf/rules/TF042.md +11 -0
- package/dist/okf/rules/TF043.md +11 -0
- package/dist/okf/rules/TF044.md +11 -0
- package/dist/okf/rules/TF045.md +11 -0
- package/dist/op/activities/index.d.ts +4 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/pin-rollout.d.ts +46 -0
- package/dist/op/activities/pin-rollout.d.ts.map +1 -0
- package/dist/op/activities/terraform.d.ts +24 -50
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activity-contracts.d.ts +48 -0
- package/dist/op/activity-contracts.d.ts.map +1 -1
- package/dist/op/builders.d.ts +9 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/pin/edit-ts.d.ts +21 -0
- package/dist/pin/edit-ts.d.ts.map +1 -0
- package/dist/pin/edit.d.ts +52 -0
- package/dist/pin/edit.d.ts.map +1 -0
- package/dist/pin/forge.d.ts +39 -0
- package/dist/pin/forge.d.ts.map +1 -0
- package/dist/pin/index.d.ts +22 -0
- package/dist/pin/index.d.ts.map +1 -0
- package/dist/pin/rollout.d.ts +116 -0
- package/dist/pin/rollout.d.ts.map +1 -0
- package/dist/pin/scan.d.ts +54 -0
- package/dist/pin/scan.d.ts.map +1 -0
- package/dist/pin/source.d.ts +93 -0
- package/dist/pin/source.d.ts.map +1 -0
- package/dist/pin/waves.d.ts +66 -0
- package/dist/pin/waves.d.ts.map +1 -0
- package/dist/plan-digest.d.ts +59 -0
- package/dist/plan-digest.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/module-source.ts +56 -1
- package/dist/rules/terragrunt.ts +76 -0
- package/dist/rules/tf002.ts +1 -1
- package/dist/rules/tf038.ts +71 -0
- package/dist/rules/tf039.ts +64 -0
- package/dist/rules/tf040.ts +76 -0
- package/dist/rules/tf041.ts +56 -0
- package/dist/rules/tf042.ts +42 -0
- package/dist/rules/tf043.ts +49 -0
- package/dist/rules/tf044.ts +51 -0
- package/dist/rules/tf045.ts +47 -0
- package/dist/terragrunt/affected.d.ts +119 -0
- package/dist/terragrunt/affected.d.ts.map +1 -0
- package/dist/terragrunt/index.d.ts +11 -0
- package/dist/terragrunt/index.d.ts.map +1 -0
- package/dist/terragrunt/mocks.d.ts +147 -0
- package/dist/terragrunt/mocks.d.ts.map +1 -0
- package/dist/terragrunt/run.d.ts +130 -0
- package/dist/terragrunt/run.d.ts.map +1 -0
- package/dist/terragrunt/units.d.ts +103 -0
- package/dist/terragrunt/units.d.ts.map +1 -0
- package/dist/terragrunt/wave.d.ts +89 -0
- package/dist/terragrunt/wave.d.ts.map +1 -0
- package/package.json +12 -2
- package/src/__fixtures__/change-set/choudoufu-set-plan.change-set.golden.json +95 -0
- package/src/__fixtures__/change-set/choudoufu-set-plan.json +76 -0
- package/src/__fixtures__/change-set/choudoufu.part.golden.json +173 -0
- package/src/__fixtures__/change-set/choudoufu.plan.json +610 -0
- package/src/__fixtures__/change-set/terraform.import-forget.plan.json +81 -0
- package/src/__fixtures__/change-set/terraform.part.golden.json +173 -0
- package/src/__fixtures__/change-set/terraform.plan.json +589 -0
- package/src/__fixtures__/change-set/tofu.part.golden.json +173 -0
- package/src/__fixtures__/change-set/tofu.plan.json +588 -0
- package/src/__fixtures__/change-set/v1.tf.txt +21 -0
- package/src/__fixtures__/change-set/v2-module-app.tf.txt +4 -0
- package/src/__fixtures__/change-set/v2.tf.txt +21 -0
- package/src/__fixtures__/pin/choudoufu-live-waves-n5.json +83 -0
- package/src/__fixtures__/pin/largeset-oci-n5/apply-order.txt +5 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e01/main.tf +33 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e02/main.tf +45 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e03/main.tf +47 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e04/main.tf +31 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/estate.chdf.hcl +6 -0
- package/src/__fixtures__/pin/largeset-oci-n5/estates/e05/main.tf +27 -0
- package/src/__fixtures__/pin/largeset-oci-n5/fixture.json +77 -0
- package/src/__fixtures__/plan-summary/largeset-n5-bump.json +2071 -0
- package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.json +95 -0
- package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.md +22 -0
- package/src/__fixtures__/plan-summary/largeset-n5.summary.golden.txt +12 -0
- package/src/__fixtures__/terragrunt/affected/catalog/units/web/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/gitignore +3 -0
- package/src/__fixtures__/terragrunt/affected/live/dev/app/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/live/dev/vpc/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/live/prod/app/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/affected/live/stg/terragrunt.stack.hcl +5 -0
- package/src/__fixtures__/terragrunt/affected/modules/app/main.tf +6 -0
- package/src/__fixtures__/terragrunt/affected/modules/app/policy.json +1 -0
- package/src/__fixtures__/terragrunt/affected/modules/net/main.tf +2 -0
- package/src/__fixtures__/terragrunt/affected/modules/thing/main.tf +3 -0
- package/src/__fixtures__/terragrunt/affected/root.hcl +5 -0
- package/src/__fixtures__/terragrunt/affected-cases.ts +126 -0
- package/src/__fixtures__/terragrunt/five-units/catalog/units/thing/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/five-units/live/dev/app/terragrunt.hcl +13 -0
- package/src/__fixtures__/terragrunt/five-units/live/dev/db/terragrunt.hcl +8 -0
- package/src/__fixtures__/terragrunt/five-units/live/dev/vpc/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/five-units/live/prod/app/terragrunt.hcl +8 -0
- package/src/__fixtures__/terragrunt/five-units/live/prod/vpc/terragrunt.hcl +3 -0
- package/src/__fixtures__/terragrunt/five-units/modules/thing/main.tf +7 -0
- package/src/__fixtures__/terragrunt/five-units/root.hcl +5 -0
- package/src/__fixtures__/terragrunt/record.ts +56 -0
- package/src/__fixtures__/terragrunt/recorded/affected-find.json +50 -0
- package/src/__fixtures__/terragrunt/recorded/failed-plan-report.json +36 -0
- package/src/__fixtures__/terragrunt/recorded/find.json +67 -0
- package/src/__fixtures__/terragrunt/recorded/version.txt +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/apply-report.json +22 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/dev/vpc/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/json/live/prod/vpc/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-1/plan-report.json +22 -0
- package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/dev/db/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-2/json/live/prod/app/tfplan.json +1 -0
- package/src/__fixtures__/terragrunt/recorded/wave-2/plan-report.json +22 -0
- package/src/change-set-combined-run.test.ts +156 -0
- package/src/change-set.test.ts +151 -0
- package/src/change-set.ts +273 -0
- package/src/changed-roots.test.ts +58 -0
- package/src/changed-roots.ts +102 -0
- package/src/commands.ts +112 -0
- package/src/components/capability-plugin.test.ts +91 -0
- package/src/components/capability-plugin.ts +131 -0
- package/src/components/pr-loop.tofu.test.ts +244 -0
- package/src/composites/terraform-pin-rollout-op.test.ts +69 -0
- package/src/composites/terraform-pin-rollout-op.ts +60 -0
- package/src/hcl/descend.ts +1 -1
- package/src/hcl/edges.ts +2 -2
- package/src/hcl/parse.test.ts +25 -0
- package/src/hcl/parse.ts +91 -6
- package/src/hcl/roots.ts +1 -1
- package/src/hcl/suppressions.ts +8 -2
- package/src/index.ts +18 -0
- package/src/lint/audit-catalog.ts +82 -0
- package/src/lint/audit.test.ts +3 -1
- package/src/lint/post-synth/fixtures/TF038/positive.tf +3 -0
- package/src/lint/post-synth/fixtures/TF039/positive.tf +4 -0
- package/src/lint/post-synth/fixtures/TF041/negative/terragrunt.hcl +12 -0
- package/src/lint/post-synth/fixtures/TF041/positive/terragrunt.hcl +7 -0
- package/src/lint/post-synth/fixtures/TF041/positive-apply/terragrunt.hcl +8 -0
- package/src/lint/post-synth/fixtures/TF042/negative/terragrunt.hcl +13 -0
- package/src/lint/post-synth/fixtures/TF042/positive/terragrunt.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF043/negative/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/negative-other/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/positive-git/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/positive-oci/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF043/positive-tfr/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF044/negative/terragrunt.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF044/positive/terragrunt.hcl +7 -0
- package/src/lint/post-synth/fixtures/TF044/positive-generate/terragrunt.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF045/negative/root.hcl +1 -0
- package/src/lint/post-synth/fixtures/TF045/negative/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF045/positive/root.hcl +9 -0
- package/src/lint/post-synth/fixtures/TF045/positive/terragrunt.hcl +3 -0
- package/src/lint/post-synth/fixtures/TF045/unit-only/terragrunt.hcl +3 -0
- package/src/lint/post-synth/index.ts +16 -0
- package/src/lint/post-synth/module-source.test.ts +42 -1
- package/src/lint/post-synth/module-source.ts +56 -1
- package/src/lint/post-synth/terragrunt.ts +76 -0
- package/src/lint/post-synth/tf002.ts +1 -1
- package/src/lint/post-synth/tf038.test.ts +51 -0
- package/src/lint/post-synth/tf038.ts +71 -0
- package/src/lint/post-synth/tf039.test.ts +40 -0
- package/src/lint/post-synth/tf039.ts +64 -0
- package/src/lint/post-synth/tf040.test.ts +55 -0
- package/src/lint/post-synth/tf040.ts +76 -0
- package/src/lint/post-synth/tf041.test.ts +50 -0
- package/src/lint/post-synth/tf041.ts +56 -0
- package/src/lint/post-synth/tf042.test.ts +19 -0
- package/src/lint/post-synth/tf042.ts +42 -0
- package/src/lint/post-synth/tf043.test.ts +65 -0
- package/src/lint/post-synth/tf043.ts +49 -0
- package/src/lint/post-synth/tf044.test.ts +27 -0
- package/src/lint/post-synth/tf044.ts +51 -0
- package/src/lint/post-synth/tf045.test.ts +30 -0
- package/src/lint/post-synth/tf045.ts +47 -0
- package/src/op/activities/index.ts +4 -0
- package/src/op/activities/pin-rollout.ts +77 -0
- package/src/op/activities/terraform.test.ts +13 -0
- package/src/op/activities/terraform.ts +46 -86
- package/src/op/activity-contracts.test.ts +4 -1
- package/src/op/activity-contracts.ts +42 -0
- package/src/op/builders.ts +13 -0
- package/src/pin/bundle.test.ts +53 -0
- package/src/pin/edit-ts.test.ts +39 -0
- package/src/pin/edit-ts.ts +77 -0
- package/src/pin/edit.test.ts +199 -0
- package/src/pin/edit.ts +157 -0
- package/src/pin/forge.ts +85 -0
- package/src/pin/index.ts +22 -0
- package/src/pin/rollout.test.ts +232 -0
- package/src/pin/rollout.ts +451 -0
- package/src/pin/scan.ts +309 -0
- package/src/pin/source.ts +206 -0
- package/src/pin/waves.test.ts +101 -0
- package/src/pin/waves.ts +125 -0
- package/src/plan-digest.ts +99 -0
- package/src/plan-summary.test.ts +154 -0
- package/src/plugin.ts +25 -2
- package/src/terragrunt/affected.acceptance.test.ts +83 -0
- package/src/terragrunt/affected.test.ts +177 -0
- package/src/terragrunt/affected.ts +348 -0
- package/src/terragrunt/index.ts +11 -0
- package/src/terragrunt/mocks.acceptance.test.ts +105 -0
- package/src/terragrunt/mocks.test.ts +309 -0
- package/src/terragrunt/mocks.ts +324 -0
- package/src/terragrunt/run.ts +369 -0
- package/src/terragrunt/terragrunt.acceptance.test.ts +122 -0
- package/src/terragrunt/terragrunt.test.ts +332 -0
- package/src/terragrunt/units.ts +235 -0
- package/src/terragrunt/wave.ts +181 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF044: a Terragrunt config keeps state in a local backend.
|
|
3
|
+
*
|
|
4
|
+
* Read from `remote_state { backend = "local" }` and from a `generate` block
|
|
5
|
+
* whose `contents` declare `backend "local"`. State on the runner's disk is
|
|
6
|
+
* gone when the job ends, a second job cannot read what the first applied, and
|
|
7
|
+
* a git-range run, which checks out each side in a temporary directory, never
|
|
8
|
+
* sees state at all. A shared backend (S3, GCS, azurerm, a Terraform HTTP
|
|
9
|
+
* backend) holds it.
|
|
10
|
+
*
|
|
11
|
+
* A backend named by an expression is not determined and passes. A local
|
|
12
|
+
* backend is sometimes right for a throwaway sandbox; suppress the rule there
|
|
13
|
+
* with `# chant-ignore TF044`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
17
|
+
import { TERRAGRUNT_GENERATE_TYPE, TERRAGRUNT_REMOTE_STATE_TYPE } from "../../hcl/parse";
|
|
18
|
+
import { terragruntEntities } from "./terragrunt";
|
|
19
|
+
|
|
20
|
+
const LOCAL_BACKEND_BLOCK = /\bbackend\s+"local"/;
|
|
21
|
+
|
|
22
|
+
export const tf044: PostSynthCheck = {
|
|
23
|
+
id: "TF044",
|
|
24
|
+
description: "Terragrunt config keeps state in a local backend",
|
|
25
|
+
|
|
26
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
27
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
28
|
+
const report = (key: string, file: string, how: string): void => {
|
|
29
|
+
diagnostics.push({
|
|
30
|
+
checkId: "TF044",
|
|
31
|
+
severity: "warning",
|
|
32
|
+
message:
|
|
33
|
+
`${how} in ${file} keeps state in a local backend. The state lives on the runner's disk, so a later job cannot read it ` +
|
|
34
|
+
"and a git-range run, which checks out each side in a temporary directory, never sees it. Use a shared backend such as S3 or GCS.",
|
|
35
|
+
entity: key,
|
|
36
|
+
lexicon: "terraform",
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
for (const block of terragruntEntities(ctx.entities, TERRAGRUNT_REMOTE_STATE_TYPE)) {
|
|
41
|
+
if (block.body.backend === "local") report(block.key, block.file, "`remote_state` sets `backend = \"local\"`");
|
|
42
|
+
}
|
|
43
|
+
for (const block of terragruntEntities(ctx.entities, TERRAGRUNT_GENERATE_TYPE)) {
|
|
44
|
+
const contents = block.body.contents;
|
|
45
|
+
if (typeof contents === "string" && LOCAL_BACKEND_BLOCK.test(contents)) {
|
|
46
|
+
report(block.key, block.file, `\`generate "${block.address.replace(/^generate\./, "")}"\` writes a \`backend "local"\` block`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return diagnostics;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { loadTreeFixture } from "./fixtures/load";
|
|
3
|
+
import { tf045 } from "./tf045";
|
|
4
|
+
|
|
5
|
+
describe("TF045: no Terragrunt config sets terragrunt_version_constraint", () => {
|
|
6
|
+
test("flags a root.hcl with none, once, anchored on root.hcl", async () => {
|
|
7
|
+
const ctx = await loadTreeFixture("TF045", "positive");
|
|
8
|
+
const diags = tf045.check(ctx);
|
|
9
|
+
expect(diags).toHaveLength(1);
|
|
10
|
+
expect(diags[0]).toMatchObject({ checkId: "TF045", severity: "warning", lexicon: "terraform" });
|
|
11
|
+
expect((ctx.entities.get(diags[0].entity!) as unknown as { props: { file: string } }).props.file).toBe("root.hcl");
|
|
12
|
+
expect(diags[0].message).toContain("root.hcl");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("a constraint in root.hcl passes", async () => {
|
|
16
|
+
expect(tf045.check(await loadTreeFixture("TF045", "negative"))).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("a unit that includes a parent that was not read is not determined", async () => {
|
|
20
|
+
expect(tf045.check(await loadTreeFixture("TF045", "unit-only"))).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("a repo with no Terragrunt config reports nothing", async () => {
|
|
24
|
+
expect(tf045.check({ outputs: new Map(), entities: new Map() } as never)).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("check metadata", () => {
|
|
28
|
+
expect(tf045.id).toBe("TF045");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF045: no Terragrunt config sets `terragrunt_version_constraint`.
|
|
3
|
+
*
|
|
4
|
+
* The attribute makes Terragrunt refuse to run on a release outside the range.
|
|
5
|
+
* Without it each machine and each CI image runs whatever Terragrunt it
|
|
6
|
+
* installed, and a behaviour change between releases reaches a plan with no
|
|
7
|
+
* diff. It is usually set once, in the root config every unit includes.
|
|
8
|
+
*
|
|
9
|
+
* The rule looks at the whole set of configs in the scan and reports once. It
|
|
10
|
+
* reports only when the set holds a config that includes no other file (the
|
|
11
|
+
* root config, or a self-contained unit) and none of the configs sets the
|
|
12
|
+
* attribute. A set made only of units that include a parent it did not read,
|
|
13
|
+
* such as one unit directory passed alone, is not determined: the parent may
|
|
14
|
+
* set it. The finding is anchored on `root.hcl` when the set has one.
|
|
15
|
+
*
|
|
16
|
+
* Off by default: the `report-only` tier keeps it out of the `recommended`
|
|
17
|
+
* preset, since many repos pin Terragrunt in the toolchain (`.tool-versions`,
|
|
18
|
+
* a CI image) rather than in config.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
22
|
+
import { TERRAGRUNT_CONFIG_TYPE } from "../../hcl/parse";
|
|
23
|
+
import { terragruntEntities } from "./terragrunt";
|
|
24
|
+
|
|
25
|
+
export const tf045: PostSynthCheck = {
|
|
26
|
+
id: "TF045",
|
|
27
|
+
description: "No Terragrunt config sets terragrunt_version_constraint",
|
|
28
|
+
|
|
29
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
30
|
+
const configs = terragruntEntities(ctx.entities, TERRAGRUNT_CONFIG_TYPE);
|
|
31
|
+
if (configs.some((c) => c.body.terragrunt_version_constraint !== undefined)) return [];
|
|
32
|
+
const tops = configs.filter((c) => Array.isArray(c.body.includes) && c.body.includes.length === 0);
|
|
33
|
+
if (tops.length === 0) return [];
|
|
34
|
+
const anchor = [...tops].sort((a, b) => Number(b.file === "root.hcl") - Number(a.file === "root.hcl") || a.key.localeCompare(b.key))[0];
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
checkId: "TF045",
|
|
38
|
+
severity: "warning",
|
|
39
|
+
message:
|
|
40
|
+
`No Terragrunt config sets \`terragrunt_version_constraint\` (${anchor.file} is the top config read). ` +
|
|
41
|
+
'Set it once in the root config, for example `terragrunt_version_constraint = ">= 1.1.0"`, so every machine runs a release the repo was tested with.',
|
|
42
|
+
entity: anchor.key,
|
|
43
|
+
lexicon: "terraform",
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* - choudoufuLiveLs: `live-ls -estate=<estate> -json [-consistent]` (#2103).
|
|
17
17
|
* - choudoufuLiveCheck: `live-check -json`, no cloud calls (#2103).
|
|
18
18
|
* - choudoufuAdopt: the tag writes that claim an adoption ledger's matches (#2105).
|
|
19
|
+
* - terraformPinRollout: one run of a pin-bump rollout, opening at most one
|
|
20
|
+
* wave's pull request (#3189). It loads the HCL parser on first call.
|
|
19
21
|
*
|
|
20
22
|
* The module is dependency-light on purpose: it shells out to the configured
|
|
21
23
|
* binary and reads the `terraform` config namespace, and never touches the
|
|
@@ -82,3 +84,5 @@ export type {
|
|
|
82
84
|
} from "./terraform";
|
|
83
85
|
|
|
84
86
|
export { detectLiveEstate } from "./live-detect";
|
|
87
|
+
export { terraformPinRollout } from "./pin-rollout";
|
|
88
|
+
export type { TerraformPinRolloutArgs, TerraformPinRolloutResult } from "./pin-rollout";
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `terraformPinRollout`: one run of a pin-bump rollout (#3189), as an Op
|
|
3
|
+
* activity. `chant terraform pin-rollout` calls the same function.
|
|
4
|
+
*
|
|
5
|
+
* Each run reads where the rollout stands on the forge and opens at most one
|
|
6
|
+
* wave's pull request (`../../pin/rollout.ts`). Run it again, by hand or on
|
|
7
|
+
* an Op `schedule`, to move on once a wave's PR has merged and its roots have
|
|
8
|
+
* applied. A run that finds a failed root or a PR closed without merging
|
|
9
|
+
* throws, naming it, so the run fails rather than reporting success.
|
|
10
|
+
*
|
|
11
|
+
* The HCL parser, the pin code and the TypeScript editor load on first call,
|
|
12
|
+
* so importing the activities loads none of them.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { isAbsolute, join } from "node:path";
|
|
17
|
+
import type { PinRoot, PinRolloutMode, PinRolloutResult } from "../../pin";
|
|
18
|
+
|
|
19
|
+
export interface TerraformPinRolloutArgs {
|
|
20
|
+
/** The module, as its `source` names it without the pin: `oci://registry.example.com/modules/vpc`. */
|
|
21
|
+
module: string;
|
|
22
|
+
/** The pin the roots are at now. */
|
|
23
|
+
from: string;
|
|
24
|
+
/** The pin to move them to: an exact version, tag, ref or `sha256:` digest. */
|
|
25
|
+
to: string;
|
|
26
|
+
/** The roots, by directory relative to the repository. Default: every root that calls the module. */
|
|
27
|
+
roots?: PinRoot[];
|
|
28
|
+
/** Roots that form wave 1. */
|
|
29
|
+
canaries?: string[];
|
|
30
|
+
/** A `choudoufu live-waves -json` document to take the waves from, instead of canaries and dependency order. */
|
|
31
|
+
wavesFrom?: string;
|
|
32
|
+
/** The directory choudoufu ran in, relative to the repository, when its roots are relative to that. */
|
|
33
|
+
wavesPrefix?: string;
|
|
34
|
+
/** `report` (default) or `pull-request`. */
|
|
35
|
+
mode?: PinRolloutMode;
|
|
36
|
+
/** The branch PRs target. Default: the remote's default branch. */
|
|
37
|
+
base?: string;
|
|
38
|
+
/** Default `origin`. */
|
|
39
|
+
remote?: string;
|
|
40
|
+
/** The apply check each root reports on the merge commit, with `{root}` for its directory. Default `apply/{root}`. */
|
|
41
|
+
appliedCheck?: string;
|
|
42
|
+
/** A directory in the repository. Default: the working directory. */
|
|
43
|
+
cwd?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type TerraformPinRolloutResult = PinRolloutResult;
|
|
47
|
+
|
|
48
|
+
/** Run one step of a pin-bump rollout. Throws when the rollout has stopped. */
|
|
49
|
+
export async function terraformPinRollout(args: TerraformPinRolloutArgs): Promise<TerraformPinRolloutResult> {
|
|
50
|
+
const result = await readPinRollout(args);
|
|
51
|
+
if (result.status === "stopped") throw new Error(`pin rollout stopped: ${result.stop}\n${result.summary}`);
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The same run, returning a stopped rollout instead of throwing. `chant terraform pin-rollout` calls this. */
|
|
56
|
+
export async function readPinRollout(args: TerraformPinRolloutArgs): Promise<TerraformPinRolloutResult> {
|
|
57
|
+
const { loadHcl2json } = await import("@intentius/chant/terraform/hcl2json");
|
|
58
|
+
const { runPinRollout, wavesFromChoudoufu } = await import("../../pin");
|
|
59
|
+
const { editPinsInTs } = await import("../../pin/edit-ts");
|
|
60
|
+
const cwd = args.cwd ?? process.cwd();
|
|
61
|
+
const waves = args.wavesFrom ? wavesFromChoudoufu(JSON.parse(readFileSync(isAbsolute(args.wavesFrom) ? args.wavesFrom : join(cwd, args.wavesFrom), "utf-8")), args.wavesPrefix) : undefined;
|
|
62
|
+
return runPinRollout({
|
|
63
|
+
module: args.module,
|
|
64
|
+
from: args.from,
|
|
65
|
+
to: args.to,
|
|
66
|
+
cwd,
|
|
67
|
+
parser: await loadHcl2json(),
|
|
68
|
+
editTs: editPinsInTs,
|
|
69
|
+
...(args.roots ? { roots: args.roots } : {}),
|
|
70
|
+
...(args.canaries ? { canaries: args.canaries } : {}),
|
|
71
|
+
...(waves ? { waves } : {}),
|
|
72
|
+
...(args.mode ? { mode: args.mode } : {}),
|
|
73
|
+
...(args.base ? { base: args.base } : {}),
|
|
74
|
+
...(args.remote ? { remote: args.remote } : {}),
|
|
75
|
+
...(args.appliedCheck ? { appliedCheck: args.appliedCheck } : {}),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -205,6 +205,19 @@ describe("terraformPlanCommand (#2086)", () => {
|
|
|
205
205
|
test("-destroy is opt-in", () => {
|
|
206
206
|
expect(terraformPlanCommand({ binary: "tofu", planFile: "p", destroy: true })).toContain(" -destroy ");
|
|
207
207
|
});
|
|
208
|
+
|
|
209
|
+
test("vars become -var flags after the var files, sorted, non-strings as JSON (#3049)", () => {
|
|
210
|
+
expect(
|
|
211
|
+
terraformPlanCommand({
|
|
212
|
+
binary: "tofu",
|
|
213
|
+
planFile: "p",
|
|
214
|
+
varFiles: ["prod.tfvars"],
|
|
215
|
+
vars: { zones: ["a", "b"], vpc_id: "vpc-1 x" },
|
|
216
|
+
}),
|
|
217
|
+
).toBe(
|
|
218
|
+
`tofu plan -input=false -detailed-exitcode -var-file=prod.tfvars -var='vpc_id=vpc-1 x' -var='zones=["a","b"]' -out=p`,
|
|
219
|
+
);
|
|
220
|
+
});
|
|
208
221
|
});
|
|
209
222
|
|
|
210
223
|
describe("terraformApplyCommand (#2086)", () => {
|
|
@@ -45,7 +45,9 @@ import { promisify } from "node:util";
|
|
|
45
45
|
import { resolve, dirname, join } from "node:path";
|
|
46
46
|
import { loadChantConfigUpward } from "@intentius/chant/config";
|
|
47
47
|
import { liveReadKey, memoLiveRead } from "@intentius/chant/live-read-session";
|
|
48
|
-
import {
|
|
48
|
+
import { terraformPlanDigest } from "../../plan-digest";
|
|
49
|
+
import { plannerForBinary, terraformChangeSetPart } from "../../change-set";
|
|
50
|
+
import type { ChangeSetPart } from "@intentius/chant/change-set";
|
|
49
51
|
import type { TerraformConfig, TerraformRootConfig } from "../../config";
|
|
50
52
|
import { detectLiveEstate } from "./live-detect";
|
|
51
53
|
import {
|
|
@@ -148,6 +150,14 @@ export interface TerraformPlanArgs extends TerraformRootArgs {
|
|
|
148
150
|
planFile?: string;
|
|
149
151
|
/** `-destroy`: plan the removal of everything the root manages. */
|
|
150
152
|
destroy?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Input variables, passed as `-var` after the root's `-var-file`s, so they
|
|
155
|
+
* win over a var file that sets the same name (#3049). A string goes as it
|
|
156
|
+
* is; anything else as JSON, which terraform reads as HCL for a list, a map
|
|
157
|
+
* or an object. This is how a value another root produced, wired with
|
|
158
|
+
* `stackOutput()`, reaches the plan.
|
|
159
|
+
*/
|
|
160
|
+
vars?: Record<string, unknown>;
|
|
151
161
|
}
|
|
152
162
|
|
|
153
163
|
export interface TerraformApplyArgs extends TerraformRootArgs {
|
|
@@ -249,91 +259,7 @@ export interface PlanChangeCounts {
|
|
|
249
259
|
destroys: number;
|
|
250
260
|
}
|
|
251
261
|
|
|
252
|
-
|
|
253
|
-
* One resource's proposed change, as a plan digest sees it (#2300) — the
|
|
254
|
-
* projection of a `terraform show -json` `resource_changes` entry that
|
|
255
|
-
* survives into {@link terraformPlanDigest}.
|
|
256
|
-
*/
|
|
257
|
-
export interface TerraformPlannedChange {
|
|
258
|
-
address: unknown;
|
|
259
|
-
mode: unknown;
|
|
260
|
-
type: unknown;
|
|
261
|
-
name: unknown;
|
|
262
|
-
index: unknown;
|
|
263
|
-
provider_name: unknown;
|
|
264
|
-
deposed: unknown;
|
|
265
|
-
actions: unknown;
|
|
266
|
-
before: unknown;
|
|
267
|
-
after: unknown;
|
|
268
|
-
after_unknown: unknown;
|
|
269
|
-
replace_paths: unknown;
|
|
270
|
-
importing: unknown;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* The change set a `terraform show -json` plan describes, canonicalised
|
|
275
|
-
* (#2300): every proposed change, at its address, with the values that would
|
|
276
|
-
* be written, sorted so two renderings of the same plan project identically.
|
|
277
|
-
*
|
|
278
|
-
* Everything else in the document is dropped, and the drops are the point —
|
|
279
|
-
* see `packages/core/src/lifecycle/plan-digest.ts` for the general rule. Here
|
|
280
|
-
* that means `timestamp` and `terraform_version` (when the plan was taken and
|
|
281
|
-
* by which binary), `format_version` (how it was rendered), `prior_state` and
|
|
282
|
-
* `planned_values` (restatements of the same change set from another angle),
|
|
283
|
-
* `configuration` (the source that produced it, which can be refactored
|
|
284
|
-
* without changing a single proposed action) and `checks` / `relevant_
|
|
285
|
-
* attributes` (diagnostics about the plan, not the plan). What is left is
|
|
286
|
-
* what applying it would do.
|
|
287
|
-
*/
|
|
288
|
-
export function terraformPlanChangeSet(planJson: unknown): {
|
|
289
|
-
resourceChanges: TerraformPlannedChange[];
|
|
290
|
-
outputChanges: unknown;
|
|
291
|
-
} {
|
|
292
|
-
const doc = planJson as { resource_changes?: unknown; output_changes?: unknown } | null | undefined;
|
|
293
|
-
const raw = Array.isArray(doc?.resource_changes) ? doc.resource_changes : [];
|
|
294
|
-
const resourceChanges = raw.map((entry): TerraformPlannedChange => {
|
|
295
|
-
const e = (entry ?? {}) as Record<string, unknown>;
|
|
296
|
-
const change = (e.change ?? {}) as Record<string, unknown>;
|
|
297
|
-
return {
|
|
298
|
-
address: e.address,
|
|
299
|
-
mode: e.mode,
|
|
300
|
-
type: e.type,
|
|
301
|
-
name: e.name,
|
|
302
|
-
index: e.index,
|
|
303
|
-
provider_name: e.provider_name,
|
|
304
|
-
deposed: e.deposed,
|
|
305
|
-
actions: change.actions,
|
|
306
|
-
before: change.before,
|
|
307
|
-
after: change.after,
|
|
308
|
-
after_unknown: change.after_unknown,
|
|
309
|
-
replace_paths: change.replace_paths,
|
|
310
|
-
importing: change.importing,
|
|
311
|
-
};
|
|
312
|
-
});
|
|
313
|
-
// terraform already emits these in address order, but nothing in the format
|
|
314
|
-
// promises it, and a digest that depends on the emitter's iteration order
|
|
315
|
-
// would refuse a plan that is identical in every way that matters.
|
|
316
|
-
resourceChanges.sort((a, b) =>
|
|
317
|
-
`${String(a.address)}\u0000${String(a.deposed ?? "")}`.localeCompare(
|
|
318
|
-
`${String(b.address)}\u0000${String(b.deposed ?? "")}`,
|
|
319
|
-
),
|
|
320
|
-
);
|
|
321
|
-
return { resourceChanges, outputChanges: doc?.output_changes ?? {} };
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* The identity of a terraform plan (#2300): {@link terraformPlanChangeSet}
|
|
326
|
-
* hashed. This is what a `TerraformApplyOp` gate binds its approval to, and
|
|
327
|
-
* what a later run recomputes and compares.
|
|
328
|
-
*
|
|
329
|
-
* Taken over the `show -json` rendering rather than the plan file's bytes on
|
|
330
|
-
* purpose: the file is an opaque, version-stamped archive, and two runs of
|
|
331
|
-
* `plan -out` over an unchanged root produce different bytes. The rendering
|
|
332
|
-
* is the same plan both times.
|
|
333
|
-
*/
|
|
334
|
-
export function terraformPlanDigest(planJson: unknown): string {
|
|
335
|
-
return computePlanDigest("terraform-plan", terraformPlanChangeSet(planJson));
|
|
336
|
-
}
|
|
262
|
+
export { terraformPlanChangeSet, terraformPlanDigest, type TerraformPlannedChange } from "../../plan-digest";
|
|
337
263
|
|
|
338
264
|
/** What {@link terraformPlan} resolved. Carries the plan itself, so no later step re-plans. */
|
|
339
265
|
export interface TerraformPlanResult extends PlanChangeCounts {
|
|
@@ -354,6 +280,13 @@ export interface TerraformPlanResult extends PlanChangeCounts {
|
|
|
354
280
|
* plan.out.planDigest })`.
|
|
355
281
|
*/
|
|
356
282
|
planDigest: string;
|
|
283
|
+
/**
|
|
284
|
+
* This root's part of a change-set document (#3181): every proposed change
|
|
285
|
+
* as an entry, the root's name as the member, and {@link planDigest} as the
|
|
286
|
+
* member's plan digest. Hand the parts of a run's roots to
|
|
287
|
+
* `composeChangeSet` and bind the run's gate to the document's digest.
|
|
288
|
+
*/
|
|
289
|
+
changeSet: ChangeSetPart;
|
|
357
290
|
}
|
|
358
291
|
|
|
359
292
|
/**
|
|
@@ -596,11 +529,16 @@ export function terraformPlanCommand(opts: {
|
|
|
596
529
|
binary: string;
|
|
597
530
|
planFile: string;
|
|
598
531
|
varFiles?: string[];
|
|
532
|
+
vars?: Record<string, unknown>;
|
|
599
533
|
destroy?: boolean;
|
|
600
534
|
}): string {
|
|
601
535
|
const parts = [opts.binary, "plan", "-input=false", "-detailed-exitcode"];
|
|
602
536
|
if (opts.destroy) parts.push("-destroy");
|
|
603
537
|
for (const varFile of opts.varFiles ?? []) parts.push(`-var-file=${quoteArg(varFile)}`);
|
|
538
|
+
for (const [name, value] of Object.entries(opts.vars ?? {}).sort(([a], [b]) => a.localeCompare(b))) {
|
|
539
|
+
if (value === undefined) continue;
|
|
540
|
+
parts.push(`-var=${quoteArg(`${name}=${typeof value === "string" ? value : JSON.stringify(value)}`)}`);
|
|
541
|
+
}
|
|
604
542
|
parts.push(`-out=${quoteArg(opts.planFile)}`);
|
|
605
543
|
return parts.join(" ");
|
|
606
544
|
}
|
|
@@ -957,6 +895,7 @@ export async function terraformPlan(
|
|
|
957
895
|
binary,
|
|
958
896
|
planFile,
|
|
959
897
|
...(root.varFiles ? { varFiles: root.varFiles } : {}),
|
|
898
|
+
...(args.vars ? { vars: args.vars } : {}),
|
|
960
899
|
...(args.destroy ? { destroy: true } : {}),
|
|
961
900
|
});
|
|
962
901
|
|
|
@@ -991,6 +930,12 @@ export async function terraformPlan(
|
|
|
991
930
|
json,
|
|
992
931
|
text: textRun.stdout,
|
|
993
932
|
planDigest: terraformPlanDigest(json),
|
|
933
|
+
changeSet: terraformChangeSetPart({
|
|
934
|
+
member: args.root,
|
|
935
|
+
plan: json,
|
|
936
|
+
planner: plannerForBinary(binary),
|
|
937
|
+
...((root.estate ?? root.workspace) ? { scope: root.estate ?? root.workspace } : {}),
|
|
938
|
+
}),
|
|
994
939
|
...countPlanChanges(json),
|
|
995
940
|
};
|
|
996
941
|
}
|
|
@@ -1069,6 +1014,21 @@ export async function terraformApply(
|
|
|
1069
1014
|
}
|
|
1070
1015
|
}
|
|
1071
1016
|
|
|
1017
|
+
/**
|
|
1018
|
+
* `terraform output -json` in the named root, as `{ name: value }` (#3049).
|
|
1019
|
+
* A sensitive output's value comes back like any other, since the caller
|
|
1020
|
+
* asked for the values to hand them on.
|
|
1021
|
+
*/
|
|
1022
|
+
export async function terraformOutputs(
|
|
1023
|
+
args: TerraformRootArgs,
|
|
1024
|
+
signal?: AbortSignal,
|
|
1025
|
+
): Promise<Record<string, unknown>> {
|
|
1026
|
+
const { binary, root, dir } = await resolveRoot(args, signal);
|
|
1027
|
+
const { stdout } = await run(`${binary} output -json`, dir, terraformEnvironment(root), signal);
|
|
1028
|
+
const parsed = JSON.parse(stdout || "{}") as Record<string, { value?: unknown }>;
|
|
1029
|
+
return Object.fromEntries(Object.entries(parsed).map(([name, entry]) => [name, entry?.value]));
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1072
1032
|
/**
|
|
1073
1033
|
* `terraform show` over current state, or over a saved plan when `planFile` is
|
|
1074
1034
|
* given. Returns the parsed `-json` output and the `-no-color` render, plus
|
|
@@ -23,6 +23,7 @@ const ACTIVITY_NAMES = [
|
|
|
23
23
|
"choudoufuLiveLs",
|
|
24
24
|
"choudoufuLiveCheck",
|
|
25
25
|
"choudoufuAdopt",
|
|
26
|
+
"terraformPinRollout",
|
|
26
27
|
];
|
|
27
28
|
|
|
28
29
|
describe("terraform activity contracts (#2101)", () => {
|
|
@@ -51,9 +52,11 @@ describe("terraform activity contracts (#2101)", () => {
|
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
test("`root` is declared as the entity every step touches (#2022)", () => {
|
|
54
|
-
for (const name of ACTIVITY_NAMES) {
|
|
55
|
+
for (const name of ACTIVITY_NAMES.filter((n) => n !== "terraformPinRollout")) {
|
|
55
56
|
expect(CONTRACTS.get(name)?.entities).toEqual(["root"]);
|
|
56
57
|
}
|
|
58
|
+
// A pin rollout names no `terraform.roots` key: it moves a module across roots found by directory.
|
|
59
|
+
expect(CONTRACTS.get("terraformPinRollout")?.entities).toBeUndefined();
|
|
57
60
|
});
|
|
58
61
|
|
|
59
62
|
// ── The failure classes the contracts exist to catch ───────────────────
|
|
@@ -76,6 +76,7 @@ export const terraformPlanContract = activityContract(
|
|
|
76
76
|
...rootArgs,
|
|
77
77
|
planFile: z.string().optional(),
|
|
78
78
|
destroy: z.boolean().optional(),
|
|
79
|
+
vars: z.record(z.string(), z.unknown()).optional(),
|
|
79
80
|
}),
|
|
80
81
|
z.object({
|
|
81
82
|
...planChangeCounts,
|
|
@@ -85,6 +86,8 @@ export const terraformPlanContract = activityContract(
|
|
|
85
86
|
json: z.unknown(),
|
|
86
87
|
text: z.string(),
|
|
87
88
|
planDigest: z.string(),
|
|
89
|
+
/** The root's change-set part (#3181); its shape is `@intentius/chant/change-set`'s `ChangeSetPart`. */
|
|
90
|
+
changeSet: z.object({ member: z.object({ member: z.string(), planDigest: z.string().nullable() }).passthrough(), entries: z.array(z.unknown()) }),
|
|
88
91
|
}),
|
|
89
92
|
rootEntity,
|
|
90
93
|
);
|
|
@@ -177,3 +180,42 @@ export const choudoufuAdoptContract = activityContract(
|
|
|
177
180
|
}),
|
|
178
181
|
rootEntity,
|
|
179
182
|
);
|
|
183
|
+
|
|
184
|
+
/** One root of a pin-bump rollout ({@link PinRoot} in `../pin/waves.ts`). */
|
|
185
|
+
const pinRoot = z.strictObject({ root: z.string(), dependsOn: z.array(z.string()).optional(), tsSource: z.string().optional() });
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* `terraformPinRollout` (#3189). It names no `terraform.roots` key: a rollout
|
|
189
|
+
* moves a module's pin across many roots, found by directory, so it carries no
|
|
190
|
+
* `rootEntity`. `status` and `summary` are the fields an Op reads: the run's
|
|
191
|
+
* outcome and a body for a report step.
|
|
192
|
+
*/
|
|
193
|
+
export const terraformPinRolloutContract = activityContract(
|
|
194
|
+
"terraformPinRollout",
|
|
195
|
+
z.strictObject({
|
|
196
|
+
module: z.string(),
|
|
197
|
+
from: z.string(),
|
|
198
|
+
to: z.string(),
|
|
199
|
+
roots: z.array(pinRoot).optional(),
|
|
200
|
+
canaries: z.array(z.string()).optional(),
|
|
201
|
+
wavesFrom: z.string().optional(),
|
|
202
|
+
wavesPrefix: z.string().optional(),
|
|
203
|
+
mode: z.enum(["report", "pull-request"]).optional(),
|
|
204
|
+
base: z.string().optional(),
|
|
205
|
+
remote: z.string().optional(),
|
|
206
|
+
appliedCheck: z.string().optional(),
|
|
207
|
+
cwd: z.string().optional(),
|
|
208
|
+
}),
|
|
209
|
+
z.object({
|
|
210
|
+
module: z.string(),
|
|
211
|
+
from: z.string(),
|
|
212
|
+
to: z.string(),
|
|
213
|
+
mode: z.enum(["report", "pull-request"]),
|
|
214
|
+
base: z.string(),
|
|
215
|
+
status: z.enum(["complete", "opened", "would-open", "waiting", "stopped"]),
|
|
216
|
+
roots: z.array(z.object({}).loose()),
|
|
217
|
+
waves: z.array(z.object({}).loose()),
|
|
218
|
+
stop: z.string().optional(),
|
|
219
|
+
summary: z.string(),
|
|
220
|
+
}),
|
|
221
|
+
);
|
package/src/op/builders.ts
CHANGED
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
ChoudoufuLiveCheckArgs,
|
|
33
33
|
ChoudoufuAdoptArgs,
|
|
34
34
|
} from "./activities/terraform";
|
|
35
|
+
import type { TerraformPinRolloutArgs } from "./activities/pin-rollout";
|
|
35
36
|
|
|
36
37
|
/** Extra opts every wrapper below accepts alongside its activity's own fields. */
|
|
37
38
|
type StepOpts = { profile?: ActivityStep["profile"]; id?: string };
|
|
@@ -184,3 +185,15 @@ export const terraformShow = (
|
|
|
184
185
|
{ profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) },
|
|
185
186
|
);
|
|
186
187
|
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* One run of a pin-bump rollout (#3189). `opts` is
|
|
191
|
+
* {@link TerraformPinRolloutArgs} itself. It names no root: the rollout finds
|
|
192
|
+
* the roots that call the module. Defaults to the `fastIdempotent` profile: a
|
|
193
|
+
* repeated run reads the forge again and finds the PR an earlier attempt
|
|
194
|
+
* opened.
|
|
195
|
+
*/
|
|
196
|
+
export const terraformPinRollout = (opts: WithStepRefs<TerraformPinRolloutArgs> & StepOpts): NamedActivityStep => {
|
|
197
|
+
const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown>);
|
|
198
|
+
return activity("terraformPinRollout", args, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
|
|
199
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pin subpath bundles small (#3189, ruling #3421).
|
|
3
|
+
*
|
|
4
|
+
* terragucci's `tf-rollout` stage runs this code from one bundled file with
|
|
5
|
+
* no TypeScript toolchain. So `@intentius/chant-lexicon-terraform/pin` must
|
|
6
|
+
* not reach `typescript`, esbuild, zod, the HCL parser's wasm package, or
|
|
7
|
+
* chant's fold, lint and codegen modules. The TypeScript editor for generated
|
|
8
|
+
* roots (`./edit-ts.ts`) stays out of it. This bundles the entry point the way
|
|
9
|
+
* terragucci would and reads esbuild's own list of what went in, the same
|
|
10
|
+
* check `packages/core/src/change-set-bundle.test.ts` makes.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { build } from "esbuild";
|
|
15
|
+
import { describe, expect, test } from "vitest";
|
|
16
|
+
|
|
17
|
+
const REPO = join(new URL(".", import.meta.url).pathname, "..", "..", "..", "..");
|
|
18
|
+
|
|
19
|
+
/** About 35 KB today, unminified. */
|
|
20
|
+
const BUDGET_BYTES = 60 * 1024;
|
|
21
|
+
|
|
22
|
+
const FORBIDDEN = [/node_modules\/typescript\//, /node_modules\/esbuild\//, /node_modules\/zod\//, /node_modules\/@cdktn\//, /\/src\/fold\//, /\/src\/lint\//, /\/src\/codegen\//, /edit-ts\.ts$/];
|
|
23
|
+
|
|
24
|
+
describe("the pin bundle", () => {
|
|
25
|
+
test("holds the edit, the wave plan and the rollout, and nothing heavier", async () => {
|
|
26
|
+
const result = await build({
|
|
27
|
+
entryPoints: [join(REPO, "lexicons", "terraform", "src", "pin", "index.ts")],
|
|
28
|
+
bundle: true,
|
|
29
|
+
platform: "node",
|
|
30
|
+
format: "esm",
|
|
31
|
+
write: false,
|
|
32
|
+
metafile: true,
|
|
33
|
+
outdir: "out",
|
|
34
|
+
logLevel: "silent",
|
|
35
|
+
});
|
|
36
|
+
const inputs = Object.keys(result.metafile.inputs);
|
|
37
|
+
for (const pattern of FORBIDDEN) {
|
|
38
|
+
expect(inputs.filter((i) => pattern.test(i)), String(pattern)).toEqual([]);
|
|
39
|
+
}
|
|
40
|
+
expect(inputs.map((i) => i.replace(/^.*?(packages|lexicons)\//, "$1/")).sort()).toEqual([
|
|
41
|
+
"lexicons/terraform/src/pin/edit.ts",
|
|
42
|
+
"lexicons/terraform/src/pin/forge.ts",
|
|
43
|
+
"lexicons/terraform/src/pin/index.ts",
|
|
44
|
+
"lexicons/terraform/src/pin/rollout.ts",
|
|
45
|
+
"lexicons/terraform/src/pin/scan.ts",
|
|
46
|
+
"lexicons/terraform/src/pin/source.ts",
|
|
47
|
+
"lexicons/terraform/src/pin/waves.ts",
|
|
48
|
+
"packages/core/src/components/layers.ts",
|
|
49
|
+
]);
|
|
50
|
+
const bytes = result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0);
|
|
51
|
+
expect(bytes).toBeLessThan(BUDGET_BYTES);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { editPinsInTs } from "./edit-ts";
|
|
3
|
+
|
|
4
|
+
const OCI = "oci://registry.example.com/modules/vpc";
|
|
5
|
+
|
|
6
|
+
describe("editPinsInTs (#3189)", () => {
|
|
7
|
+
test("moves the pin in a module declaration and leaves every other byte", () => {
|
|
8
|
+
const before = [
|
|
9
|
+
`// source: "${OCI}?tag=1.3.0" in a comment is not a declaration`,
|
|
10
|
+
`export const vpc = module("vpc", {`,
|
|
11
|
+
` source: '${OCI}?tag=1.3.0',`,
|
|
12
|
+
` cidr: "10.0.0.0/16",`,
|
|
13
|
+
`});`,
|
|
14
|
+
`export const registry = new TerraformModule({ source: "app.terraform.io/acme/vpc/aws", version: "= 1.3.0" });`,
|
|
15
|
+
`export const other = module("dns", { source: "${OCI.replace("vpc", "dns")}?tag=1.3.0" });`,
|
|
16
|
+
``,
|
|
17
|
+
].join("\n");
|
|
18
|
+
const after = editPinsInTs(before, "infra/vpc.ts", { module: OCI, from: "1.3.0", to: "1.4.0" });
|
|
19
|
+
expect(after.content).toBe(before.replace(`'${OCI}?tag=1.3.0'`, `'${OCI}?tag=1.4.0'`));
|
|
20
|
+
expect(after.calls).toMatchObject([{ outcome: "moved", file: "infra/vpc.ts", call: "line 2" }]);
|
|
21
|
+
|
|
22
|
+
const registry = editPinsInTs(before, "infra/vpc.ts", { module: "app.terraform.io/acme/vpc/aws", from: "1.3.0", to: "1.4.0" });
|
|
23
|
+
expect(registry.content).toBe(before.replace(`version: "= 1.3.0"`, `version: "= 1.4.0"`));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("refuses a floating constraint with its reason", () => {
|
|
27
|
+
const before = `export const m = { source: "app.terraform.io/acme/vpc/aws", version: "~> 1.3" };\n`;
|
|
28
|
+
const result = editPinsInTs(before, "m.ts", { module: "app.terraform.io/acme/vpc/aws", from: "1.3.0", to: "1.4.0" });
|
|
29
|
+
expect(result.content).toBe(before);
|
|
30
|
+
expect(result.calls).toMatchObject([{ outcome: "refused", reason: expect.stringMatching(/is a constraint, not a pin/) }]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("refuses a version built from an expression", () => {
|
|
34
|
+
const before = `const v = "1.3.0";\nexport const m = { source: "app.terraform.io/acme/vpc/aws", version: v };\n`;
|
|
35
|
+
const result = editPinsInTs(before, "m.ts", { module: "app.terraform.io/acme/vpc/aws", from: "1.3.0", to: "1.4.0" });
|
|
36
|
+
expect(result.content).toBe(before);
|
|
37
|
+
expect(result.calls).toMatchObject([{ outcome: "refused", reason: expect.stringMatching(/expression/) }]);
|
|
38
|
+
});
|
|
39
|
+
});
|