@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,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF038: an `oci://` module source names no tag and no digest, or a mutable
|
|
3
|
+
* tag.
|
|
4
|
+
*
|
|
5
|
+
* OpenTofu resolves an OCI module source with no `tag` to the `latest` tag
|
|
6
|
+
* (https://opentofu.org/docs/language/modules/sources/), so an unpinned source
|
|
7
|
+
* and `?tag=latest` are the same thing: a push to the registry changes what
|
|
8
|
+
* the root provisions with no diff to review. A digest, or an exact version
|
|
9
|
+
* tag, holds it. `classifyModuleSource` (`./module-source.ts`) reads the tag
|
|
10
|
+
* and digest from either spelling.
|
|
11
|
+
*
|
|
12
|
+
* A warning rather than an error: a registry can make a version tag
|
|
13
|
+
* immutable, and a source that names `?tag=1.4.0` is read as pinned here
|
|
14
|
+
* whether or not the registry enforces that. A digest is the only pin the
|
|
15
|
+
* registry cannot move, which the message says. A source that names a digest
|
|
16
|
+
* is never flagged, whatever tag sits beside it.
|
|
17
|
+
*
|
|
18
|
+
* Scope: root and child modules alike (#2112), same as TF004 and TF005.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
PostSynthCheck,
|
|
23
|
+
PostSynthContext,
|
|
24
|
+
PostSynthDiagnostic,
|
|
25
|
+
} from "@intentius/chant/lint/post-synth";
|
|
26
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
27
|
+
import { MODULE_TYPE, type BlockBody } from "../../hcl/parse";
|
|
28
|
+
import { classifyModuleSource, isMutableOciTag } from "./module-source";
|
|
29
|
+
|
|
30
|
+
export const tf038: PostSynthCheck = {
|
|
31
|
+
id: "TF038",
|
|
32
|
+
description: "OCI module source has no tag or digest, or a mutable tag",
|
|
33
|
+
|
|
34
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
35
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
36
|
+
|
|
37
|
+
for (const [key, entity] of ctx.entities) {
|
|
38
|
+
if (entity.entityType !== MODULE_TYPE || !isResourceDeclarable(entity)) continue;
|
|
39
|
+
const props = entity.props as { address?: unknown; body?: unknown };
|
|
40
|
+
const body = (typeof props.body === "object" && props.body !== null ? props.body : {}) as BlockBody;
|
|
41
|
+
const source = typeof body.source === "string" ? body.source : undefined;
|
|
42
|
+
if (source === undefined) continue;
|
|
43
|
+
|
|
44
|
+
const classified = classifyModuleSource(source);
|
|
45
|
+
if (classified.kind !== "oci" || classified.digest) continue;
|
|
46
|
+
|
|
47
|
+
const address = typeof props.address === "string" ? props.address : key;
|
|
48
|
+
let message: string | undefined;
|
|
49
|
+
if (!classified.tag) {
|
|
50
|
+
message =
|
|
51
|
+
`Module "${address}" sources "${source}" with no tag or digest, so OpenTofu pulls the \`latest\` tag. ` +
|
|
52
|
+
"Pin it to a digest (`?digest=sha256:...`) or an exact version tag (`?tag=1.4.0`).";
|
|
53
|
+
} else if (isMutableOciTag(classified.tag)) {
|
|
54
|
+
message =
|
|
55
|
+
`Module "${address}" sources "${source}" with the mutable tag "${classified.tag}". ` +
|
|
56
|
+
"Pin it to a digest (`?digest=sha256:...`) or an exact version tag (`?tag=1.4.0`).";
|
|
57
|
+
}
|
|
58
|
+
if (!message) continue;
|
|
59
|
+
|
|
60
|
+
diagnostics.push({
|
|
61
|
+
checkId: "TF038",
|
|
62
|
+
severity: "warning",
|
|
63
|
+
message,
|
|
64
|
+
entity: key,
|
|
65
|
+
lexicon: "terraform",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return diagnostics;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { blocksToEntities } from "../../hcl/parse";
|
|
4
|
+
import { loadFixture } from "./fixtures/load";
|
|
5
|
+
import { tf039 } from "./tf039";
|
|
6
|
+
import { terraformPlugin } from "../../plugin";
|
|
7
|
+
|
|
8
|
+
async function withVersion(version: string | undefined, source = "terraform-aws-modules/vpc/aws"): Promise<PostSynthContext> {
|
|
9
|
+
const hcl = `module "vpc" {\n source = "${source}"\n${version === undefined ? "" : ` version = ${JSON.stringify(version)}\n`}}\n`;
|
|
10
|
+
const entities = await blocksToEntities([{ name: "main.tf", source: hcl }], "app");
|
|
11
|
+
return { outputs: new Map(), entities } as unknown as PostSynthContext;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("TF039: registry module version is a range", () => {
|
|
15
|
+
test("flags the documented positive fixture", async () => {
|
|
16
|
+
expect(tf039.check(await loadFixture("TF039", "positive"))).toHaveLength(1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test.each(["~> 1.4", ">= 1.4", ">= 1.0, < 2.0", "< 2.0", "!= 1.4.0"])("flags %s", async (v) => {
|
|
20
|
+
const diags = tf039.check(await withVersion(v));
|
|
21
|
+
expect(diags).toHaveLength(1);
|
|
22
|
+
expect(diags[0]).toMatchObject({ checkId: "TF039", severity: "warning", entity: "app/module.vpc" });
|
|
23
|
+
expect(diags[0].message).toContain(v);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test.each(["1.4.0", "= 1.4.0", "=1.4.0"])("passes %s", async (v) => {
|
|
27
|
+
expect(tf039.check(await withVersion(v))).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("leaves a missing version to TF004, and non-registry sources alone", async () => {
|
|
31
|
+
expect(tf039.check(await withVersion(undefined))).toEqual([]);
|
|
32
|
+
expect(tf039.check(await withVersion("~> 1.0", "./modules/x"))).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("is off by default: in the all preset, not in recommended", () => {
|
|
36
|
+
const presets = terraformPlugin.lintPresets!();
|
|
37
|
+
expect(presets.all).toContain("TF039");
|
|
38
|
+
expect(presets.recommended).not.toContain("TF039");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF039: a registry module's `version` is a range, not an exact version.
|
|
3
|
+
*
|
|
4
|
+
* TF004 flags a registry module with no `version`; a `version` of `~> 1.4`,
|
|
5
|
+
* `>= 1.4` or `>= 1.0, < 2.0` passes it, yet an upstream release inside the
|
|
6
|
+
* range changes what the root provisions with no diff to review. This rule is
|
|
7
|
+
* for projects that pin modules to one version (`1.4.0` or `= 1.4.0`).
|
|
8
|
+
*
|
|
9
|
+
* Off by default: it is a `report-only` catalog entry, so the `recommended`
|
|
10
|
+
* preset leaves it out and `lint.presets: { terraform: "all" }` (or a
|
|
11
|
+
* `lint.rules` entry for TF039) turns it on. Ranges are common and often
|
|
12
|
+
* intended, so a default warning would be noise for most roots. It is a
|
|
13
|
+
* separate rule from TF004 because a post-synth check receives no options
|
|
14
|
+
* (`PostSynthCheck.check(ctx)`), while the preset mechanism already carries
|
|
15
|
+
* exactly this on/off choice per rule id (#3190).
|
|
16
|
+
*
|
|
17
|
+
* A module that has no `version` is TF004's finding and is skipped here.
|
|
18
|
+
*
|
|
19
|
+
* Scope: root and child modules alike (#2112).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type {
|
|
23
|
+
PostSynthCheck,
|
|
24
|
+
PostSynthContext,
|
|
25
|
+
PostSynthDiagnostic,
|
|
26
|
+
} from "@intentius/chant/lint/post-synth";
|
|
27
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
28
|
+
import { MODULE_TYPE, type BlockBody } from "../../hcl/parse";
|
|
29
|
+
import { classifyModuleSource, isExactVersionConstraint } from "./module-source";
|
|
30
|
+
|
|
31
|
+
export const tf039: PostSynthCheck = {
|
|
32
|
+
id: "TF039",
|
|
33
|
+
description: "Registry module version is a range, not an exact version",
|
|
34
|
+
|
|
35
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
36
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
37
|
+
|
|
38
|
+
for (const [key, entity] of ctx.entities) {
|
|
39
|
+
if (entity.entityType !== MODULE_TYPE || !isResourceDeclarable(entity)) continue;
|
|
40
|
+
const props = entity.props as { address?: unknown; body?: unknown };
|
|
41
|
+
const body = (typeof props.body === "object" && props.body !== null ? props.body : {}) as BlockBody;
|
|
42
|
+
const source = typeof body.source === "string" ? body.source : undefined;
|
|
43
|
+
if (source === undefined || classifyModuleSource(source).kind !== "registry") continue;
|
|
44
|
+
|
|
45
|
+
const version = body.version;
|
|
46
|
+
if (typeof version !== "string" || version.trim() === "") continue;
|
|
47
|
+
if (isExactVersionConstraint(version)) continue;
|
|
48
|
+
|
|
49
|
+
const address = typeof props.address === "string" ? props.address : key;
|
|
50
|
+
diagnostics.push({
|
|
51
|
+
checkId: "TF039",
|
|
52
|
+
severity: "warning",
|
|
53
|
+
message:
|
|
54
|
+
`Module "${address}" sources "${source}" with the version range "${version}". ` +
|
|
55
|
+
"A release inside the range changes what the root provisions with no diff to review. " +
|
|
56
|
+
'Pin an exact version (`version = "1.4.0"`).',
|
|
57
|
+
entity: key,
|
|
58
|
+
lexicon: "terraform",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return diagnostics;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
6
|
+
import { blocksToEntities, parseTerraformRootDir } from "../../hcl/parse";
|
|
7
|
+
import { tf040 } from "./tf040";
|
|
8
|
+
import { terraformPlugin } from "../../plugin";
|
|
9
|
+
|
|
10
|
+
const dirs: string[] = [];
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function root(files: Record<string, string>): string {
|
|
16
|
+
const dir = mkdtempSync(join(tmpdir(), "tf040-"));
|
|
17
|
+
dirs.push(dir);
|
|
18
|
+
for (const [name, text] of Object.entries(files)) writeFileSync(join(dir, name), text);
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const MAIN = 'resource "aws_s3_bucket" "b" {\n bucket = "x"\n}\n';
|
|
23
|
+
|
|
24
|
+
async function ctxOf(dir: string): Promise<PostSynthContext> {
|
|
25
|
+
const entities = await parseTerraformRootDir(dir, "app");
|
|
26
|
+
return { outputs: new Map(), entities } as unknown as PostSynthContext;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("TF040: root has no committed .terraform.lock.hcl", () => {
|
|
30
|
+
test("flags a root with providers and no lock file, once", async () => {
|
|
31
|
+
const diags = tf040.check(await ctxOf(root({ "main.tf": MAIN + MAIN.replace('"b"', '"c"') })));
|
|
32
|
+
expect(diags).toHaveLength(1);
|
|
33
|
+
expect(diags[0]).toMatchObject({ checkId: "TF040", severity: "warning", lexicon: "terraform" });
|
|
34
|
+
expect(diags[0].message).toContain('"aws"');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("passes a root with the lock file beside its .tf files", async () => {
|
|
38
|
+
expect(tf040.check(await ctxOf(root({ "main.tf": MAIN, ".terraform.lock.hcl": "# lock\n" })))).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("skips a root that implies no provider", async () => {
|
|
42
|
+
expect(tf040.check(await ctxOf(root({ "main.tf": 'variable "x" {}\n' })))).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("reports nothing when the parse had no directory (not determined)", async () => {
|
|
46
|
+
const entities = await blocksToEntities([{ name: "main.tf", source: MAIN }], "app");
|
|
47
|
+
expect(tf040.check({ outputs: new Map(), entities } as unknown as PostSynthContext)).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("is off by default: in the all preset, not in recommended", () => {
|
|
51
|
+
const presets = terraformPlugin.lintPresets!();
|
|
52
|
+
expect(presets.all).toContain("TF040");
|
|
53
|
+
expect(presets.recommended).not.toContain("TF040");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF040: a root that uses providers has no `.terraform.lock.hcl`.
|
|
3
|
+
*
|
|
4
|
+
* The lock file records the provider versions and hashes `terraform init`
|
|
5
|
+
* selected. Without it, each `init` resolves `required_providers` constraints
|
|
6
|
+
* afresh, so a provider release changes a plan with no diff. None of
|
|
7
|
+
* TF001 to TF029 looks at it.
|
|
8
|
+
*
|
|
9
|
+
* "Committed" is decided by presence: the file sits beside the root's `.tf`
|
|
10
|
+
* files in the source tree being parsed (`props.lockFile`, stamped by
|
|
11
|
+
* `parseTerraformRootDir` and by the local audit path). Whether git tracks it
|
|
12
|
+
* is not asked, so a lock file that is present but gitignored counts as
|
|
13
|
+
* committed. A parse with no directory to look in (a fetched repository,
|
|
14
|
+
* inline source) leaves `lockFile` unset and the rule reports nothing: not
|
|
15
|
+
* determined, not clean.
|
|
16
|
+
*
|
|
17
|
+
* A root that implies no provider (no `provider` block and no resource or data
|
|
18
|
+
* type) gets no lock file from `init`, so it is skipped, as is a live
|
|
19
|
+
* choudoufu root, which runs no `init`. One diagnostic per root, anchored on
|
|
20
|
+
* the first block that implies a provider.
|
|
21
|
+
*
|
|
22
|
+
* Off by default in both places: the `report-only` tier keeps it out of a
|
|
23
|
+
* build's `recommended` preset, and `auditOptIn` keeps it out of `chant audit`.
|
|
24
|
+
* Many estates run `init` in CI and never commit the file.
|
|
25
|
+
*
|
|
26
|
+
* Scope: root modules only (#2112); a child module has no lock file of its own.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type {
|
|
30
|
+
PostSynthCheck,
|
|
31
|
+
PostSynthContext,
|
|
32
|
+
PostSynthDiagnostic,
|
|
33
|
+
} from "@intentius/chant/lint/post-synth";
|
|
34
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
35
|
+
import { LOCK_FILENAME } from "../../hcl/parse";
|
|
36
|
+
import { isRootScoped } from "./scope";
|
|
37
|
+
import { impliedProviders } from "./tf002";
|
|
38
|
+
|
|
39
|
+
export const tf040: PostSynthCheck = {
|
|
40
|
+
id: "TF040",
|
|
41
|
+
description: "Root has no committed .terraform.lock.hcl",
|
|
42
|
+
// `chant audit` runs every rule, report-only ones included, so without this a
|
|
43
|
+
// new rule would change every project's audit output. A `lint.rules` entry
|
|
44
|
+
// for TF040 turns it on in the audit as well as in a build (#3190).
|
|
45
|
+
auditOptIn: true,
|
|
46
|
+
|
|
47
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
48
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
49
|
+
const missing = new Set<string>();
|
|
50
|
+
|
|
51
|
+
for (const entity of ctx.entities.values()) {
|
|
52
|
+
if (!isResourceDeclarable(entity) || !isRootScoped(entity)) continue;
|
|
53
|
+
const props = entity.props as { root?: unknown; lockFile?: unknown; mode?: unknown };
|
|
54
|
+
if (props.lockFile !== false || props.mode === "live" || typeof props.root !== "string") continue;
|
|
55
|
+
missing.add(props.root);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const root of [...missing].sort()) {
|
|
59
|
+
const implied = impliedProviders(ctx.entities, root);
|
|
60
|
+
if (implied.size === 0) continue;
|
|
61
|
+
const names = [...implied.keys()].sort();
|
|
62
|
+
diagnostics.push({
|
|
63
|
+
checkId: "TF040",
|
|
64
|
+
severity: "warning",
|
|
65
|
+
message:
|
|
66
|
+
`Root module "${root}" uses ${names.length === 1 ? "provider" : "providers"} ${names.map((n) => `"${n}"`).join(", ")} ` +
|
|
67
|
+
`but has no ${LOCK_FILENAME} beside its .tf files. Run \`terraform init\` and commit the file, ` +
|
|
68
|
+
"so every init selects the same provider versions.",
|
|
69
|
+
entity: implied.get(names[0]),
|
|
70
|
+
lexicon: "terraform",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return diagnostics;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { blocksToEntities } from "../../hcl/parse";
|
|
4
|
+
import { loadTreeFixture } from "./fixtures/load";
|
|
5
|
+
import { tf041 } from "./tf041";
|
|
6
|
+
|
|
7
|
+
async function withDependency(body: string): Promise<PostSynthContext> {
|
|
8
|
+
const hcl = `dependency "vpc" {\n config_path = "../vpc"\n${body}\n}\n`;
|
|
9
|
+
const entities = await blocksToEntities([{ name: "terragrunt.hcl", source: hcl }], "unit");
|
|
10
|
+
return { outputs: new Map(), entities } as unknown as PostSynthContext;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("TF041: Terragrunt dependency lets mock_outputs stand in for apply", () => {
|
|
14
|
+
test("mocks with no allow-list", async () => {
|
|
15
|
+
const diags = tf041.check(await loadTreeFixture("TF041", "positive"));
|
|
16
|
+
expect(diags).toHaveLength(1);
|
|
17
|
+
expect(diags[0]).toMatchObject({ checkId: "TF041", severity: "warning", entity: "TF041/dependency.vpc", lexicon: "terraform" });
|
|
18
|
+
expect(diags[0].message).toContain("no `mock_outputs_allowed_terraform_commands`");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("an allow-list that names apply", async () => {
|
|
22
|
+
const diags = tf041.check(await loadTreeFixture("TF041", "positive-apply"));
|
|
23
|
+
expect(diags).toHaveLength(1);
|
|
24
|
+
expect(diags[0].message).toContain('lists "apply"');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("a restricted list and a dependency with no mocks pass", async () => {
|
|
28
|
+
expect(tf041.check(await loadTreeFixture("TF041", "negative"))).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("an empty list restricts nothing, so it is flagged", async () => {
|
|
32
|
+
const diags = tf041.check(await withDependency(" mock_outputs = { id = \"x\" }\n mock_outputs_allowed_terraform_commands = []"));
|
|
33
|
+
expect(diags).toHaveLength(1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a list built from an expression is not determined", async () => {
|
|
37
|
+
const body = ' mock_outputs = { id = "x" }\n mock_outputs_allowed_terraform_commands = local.mock_commands';
|
|
38
|
+
expect(tf041.check(await withDependency(body))).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("the dependency block records its start line, which anchors chant-ignore", async () => {
|
|
42
|
+
const ctx = await withDependency(' mock_outputs = { id = "x" }');
|
|
43
|
+
const entity = ctx.entities.get("unit/dependency.vpc") as unknown as { props: { line?: number } };
|
|
44
|
+
expect(entity.props.line).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("check metadata", () => {
|
|
48
|
+
expect(tf041.id).toBe("TF041");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF041: a Terragrunt `dependency` has `mock_outputs` and lets them stand in
|
|
3
|
+
* for `apply`.
|
|
4
|
+
*
|
|
5
|
+
* `mock_outputs` is what a dependency returns when it has no outputs yet.
|
|
6
|
+
* `mock_outputs_allowed_terraform_commands` limits which commands may use
|
|
7
|
+
* them. Terragrunt reads a missing list and an empty list the same way, as no
|
|
8
|
+
* limit (`shouldReturnMockOutputs` in `pkg/config/dependency.go`, v1.1.6:
|
|
9
|
+
* `nil || len == 0 || slices.Contains(list, command)`). So a dependency with
|
|
10
|
+
* mocks and no list, or a list that names `apply`, can apply the mock values
|
|
11
|
+
* to real state when the upstream unit has not been applied, and the apply
|
|
12
|
+
* reports success. The usual list is `["validate", "plan"]`.
|
|
13
|
+
*
|
|
14
|
+
* A list that is an expression (`local.mock_commands`) is not determined and
|
|
15
|
+
* passes. A dependency with `enabled = false` returns its mocks whatever the
|
|
16
|
+
* list says; that is a different decision and is not reported here.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
20
|
+
import { TERRAGRUNT_DEPENDENCY_TYPE } from "../../hcl/parse";
|
|
21
|
+
import { isExpression, terragruntEntities } from "./terragrunt";
|
|
22
|
+
|
|
23
|
+
export const tf041: PostSynthCheck = {
|
|
24
|
+
id: "TF041",
|
|
25
|
+
description: "Terragrunt dependency lets mock_outputs stand in for apply",
|
|
26
|
+
|
|
27
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
28
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
29
|
+
for (const dep of terragruntEntities(ctx.entities, TERRAGRUNT_DEPENDENCY_TYPE)) {
|
|
30
|
+
if (dep.body.mock_outputs === undefined) continue;
|
|
31
|
+
const allowed = dep.body.mock_outputs_allowed_terraform_commands;
|
|
32
|
+
if (isExpression(allowed)) continue;
|
|
33
|
+
|
|
34
|
+
let problem: string | undefined;
|
|
35
|
+
if (allowed === undefined || (Array.isArray(allowed) && allowed.length === 0)) {
|
|
36
|
+
problem =
|
|
37
|
+
"sets no `mock_outputs_allowed_terraform_commands`, and Terragrunt reads a missing or empty list as every command";
|
|
38
|
+
} else if (Array.isArray(allowed) && allowed.includes("apply")) {
|
|
39
|
+
problem = 'lists "apply" in `mock_outputs_allowed_terraform_commands`';
|
|
40
|
+
}
|
|
41
|
+
if (problem === undefined) continue;
|
|
42
|
+
|
|
43
|
+
diagnostics.push({
|
|
44
|
+
checkId: "TF041",
|
|
45
|
+
severity: "warning",
|
|
46
|
+
message:
|
|
47
|
+
`Dependency "${dep.address.replace(/^dependency\./, "")}" in ${dep.file} sets \`mock_outputs\` and ${problem}. ` +
|
|
48
|
+
"An apply that runs before the upstream unit is applied writes the mock values to real state. " +
|
|
49
|
+
'Set `mock_outputs_allowed_terraform_commands = ["validate", "plan"]`.',
|
|
50
|
+
entity: dep.key,
|
|
51
|
+
lexicon: "terraform",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return diagnostics;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { loadTreeFixture } from "./fixtures/load";
|
|
3
|
+
import { tf042 } from "./tf042";
|
|
4
|
+
|
|
5
|
+
describe("TF042: Terragrunt dependency sets skip_outputs together with mock_outputs", () => {
|
|
6
|
+
test("flags the pair", async () => {
|
|
7
|
+
const diags = tf042.check(await loadTreeFixture("TF042", "positive"));
|
|
8
|
+
expect(diags).toHaveLength(1);
|
|
9
|
+
expect(diags[0]).toMatchObject({ checkId: "TF042", severity: "warning", entity: "TF042/dependency.vpc", lexicon: "terraform" });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("skip_outputs alone, and skip_outputs = false with mocks, pass", async () => {
|
|
13
|
+
expect(tf042.check(await loadTreeFixture("TF042", "negative"))).toEqual([]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("check metadata", () => {
|
|
17
|
+
expect(tf042.id).toBe("TF042");
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF042: a Terragrunt `dependency` sets `skip_outputs = true` and
|
|
3
|
+
* `mock_outputs`.
|
|
4
|
+
*
|
|
5
|
+
* With `skip_outputs = true` Terragrunt never reads the upstream unit's
|
|
6
|
+
* outputs (`shouldGetOutputs` in `pkg/config/dependency.go`, v1.1.6), so the
|
|
7
|
+
* only values the dependency can return are the mocks. Every plan and apply
|
|
8
|
+
* the mocks are allowed for runs on them, however far the upstream unit has
|
|
9
|
+
* been applied. The pair is sometimes meant, to break a cycle or to avoid
|
|
10
|
+
* reading state, and then the mock values are the configuration; that is
|
|
11
|
+
* worth writing as inputs, which a reader of the unit can see.
|
|
12
|
+
*
|
|
13
|
+
* Only a literal `skip_outputs = true` is reported. A `skip_outputs` built
|
|
14
|
+
* from an expression is not determined.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
18
|
+
import { TERRAGRUNT_DEPENDENCY_TYPE } from "../../hcl/parse";
|
|
19
|
+
import { terragruntEntities } from "./terragrunt";
|
|
20
|
+
|
|
21
|
+
export const tf042: PostSynthCheck = {
|
|
22
|
+
id: "TF042",
|
|
23
|
+
description: "Terragrunt dependency sets skip_outputs together with mock_outputs",
|
|
24
|
+
|
|
25
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
26
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
27
|
+
for (const dep of terragruntEntities(ctx.entities, TERRAGRUNT_DEPENDENCY_TYPE)) {
|
|
28
|
+
if (dep.body.skip_outputs !== true || dep.body.mock_outputs === undefined) continue;
|
|
29
|
+
diagnostics.push({
|
|
30
|
+
checkId: "TF042",
|
|
31
|
+
severity: "warning",
|
|
32
|
+
message:
|
|
33
|
+
`Dependency "${dep.address.replace(/^dependency\./, "")}" in ${dep.file} sets \`skip_outputs = true\` with \`mock_outputs\`. ` +
|
|
34
|
+
"Terragrunt then never reads the upstream unit's outputs, so every plan and apply runs on the mock values. " +
|
|
35
|
+
"Drop `skip_outputs`, or pass the values as `inputs` so they are visible as configuration.",
|
|
36
|
+
entity: dep.key,
|
|
37
|
+
lexicon: "terraform",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return diagnostics;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { blocksToEntities } from "../../hcl/parse";
|
|
4
|
+
import { loadTreeFixture } from "./fixtures/load";
|
|
5
|
+
import { classifyTerragruntSource } from "./terragrunt";
|
|
6
|
+
import { tf043 } from "./tf043";
|
|
7
|
+
|
|
8
|
+
async function withSource(source: string): Promise<PostSynthContext> {
|
|
9
|
+
const hcl = `terraform {\n source = ${JSON.stringify(source)}\n}\n`;
|
|
10
|
+
const entities = await blocksToEntities([{ name: "terragrunt.hcl", source: hcl }], "unit");
|
|
11
|
+
return { outputs: new Map(), entities } as unknown as PostSynthContext;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("TF043: Terragrunt terraform.source names no version", () => {
|
|
15
|
+
test.each([
|
|
16
|
+
["positive-git", "?ref="],
|
|
17
|
+
["positive-tfr", "?version="],
|
|
18
|
+
["positive-oci", "?digest="],
|
|
19
|
+
])("flags the %s fixture", async (name, fix) => {
|
|
20
|
+
const diags = tf043.check(await loadTreeFixture("TF043", name));
|
|
21
|
+
expect(diags).toHaveLength(1);
|
|
22
|
+
expect(diags[0]).toMatchObject({ checkId: "TF043", severity: "warning", entity: "TF043/terragrunt.terraform", lexicon: "terraform" });
|
|
23
|
+
expect(diags[0].message).toContain(fix);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("a pinned git source and an interpolated source pass", async () => {
|
|
27
|
+
expect(tf043.check(await loadTreeFixture("TF043", "negative"))).toEqual([]);
|
|
28
|
+
expect(tf043.check(await loadTreeFixture("TF043", "negative-other"))).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test.each([
|
|
32
|
+
"github.com/example/modules//vpc",
|
|
33
|
+
"git@github.com:example/modules.git//vpc",
|
|
34
|
+
"git::ssh://git@example.com/org/modules.git",
|
|
35
|
+
"tfr://registry.example.com/terraform-aws-modules/vpc/aws",
|
|
36
|
+
"tfr:///terraform-aws-modules/vpc/aws?version=",
|
|
37
|
+
"oci://registry.example.com:5000/org/mod",
|
|
38
|
+
])("flags %s", async (source) => {
|
|
39
|
+
expect(tf043.check(await withSource(source))).toHaveLength(1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test.each([
|
|
43
|
+
"github.com/example/modules//vpc?ref=v1.0.0",
|
|
44
|
+
"git::https://example.com/org/modules.git//vpc?ref=0123456789abcdef0123456789abcdef01234567",
|
|
45
|
+
"tfr:///terraform-aws-modules/vpc/aws?version=5.1.0",
|
|
46
|
+
"tfr:///terraform-aws-modules/vpc/aws//modules/x?version=5.1.0",
|
|
47
|
+
"oci://registry.example.com/org/mod?tag=1.4.0",
|
|
48
|
+
"oci://registry.example.com/org/mod@sha256:abc123",
|
|
49
|
+
"../modules/vpc",
|
|
50
|
+
"/abs/modules/vpc",
|
|
51
|
+
"${local.base}//vpc",
|
|
52
|
+
"https://example.com/modules.zip",
|
|
53
|
+
])("passes %s", async (source) => {
|
|
54
|
+
expect(tf043.check(await withSource(source))).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("classification", () => {
|
|
58
|
+
expect(classifyTerragruntSource("tfr:///a/b/c?version=1.0.0")).toEqual({ kind: "tfr", pinned: true });
|
|
59
|
+
expect(classifyTerragruntSource("./x")).toEqual({ kind: "local" });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("check metadata", () => {
|
|
63
|
+
expect(tf043.id).toBe("TF043");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF043: a Terragrunt unit's `terraform.source` names no version.
|
|
3
|
+
*
|
|
4
|
+
* A git source with no `?ref=` follows the default branch, a `tfr://` source
|
|
5
|
+
* with no `?version=` follows the registry's latest release, and an `oci://`
|
|
6
|
+
* source with no tag or digest pulls `latest`. A push or a release changes
|
|
7
|
+
* what the unit provisions with no diff to review. TF005 and TF038 do the same
|
|
8
|
+
* for `module` blocks; a Terragrunt unit has no `module` block, its source is
|
|
9
|
+
* the `source` argument of the `terraform` block.
|
|
10
|
+
*
|
|
11
|
+
* A local path and a source built from an expression (`${local.base}//x`) are
|
|
12
|
+
* not determined and pass. A mutable `?ref=main` is not judged
|
|
13
|
+
* here: the rule asks only whether a version is named.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
17
|
+
import { TERRAGRUNT_SOURCE_TYPE } from "../../hcl/parse";
|
|
18
|
+
import { classifyTerragruntSource, terragruntEntities } from "./terragrunt";
|
|
19
|
+
|
|
20
|
+
const FIX: Record<string, string> = {
|
|
21
|
+
git: "Add `?ref=` with a tag or commit SHA.",
|
|
22
|
+
tfr: "Add `?version=` with an exact version.",
|
|
23
|
+
oci: "Add `?digest=sha256:...` or `?tag=` with an exact version tag.",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const tf043: PostSynthCheck = {
|
|
27
|
+
id: "TF043",
|
|
28
|
+
description: "Terragrunt terraform.source names no version (git ref, tfr version or oci tag)",
|
|
29
|
+
|
|
30
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
31
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
32
|
+
for (const block of terragruntEntities(ctx.entities, TERRAGRUNT_SOURCE_TYPE)) {
|
|
33
|
+
const source = block.body.source;
|
|
34
|
+
if (typeof source !== "string") continue;
|
|
35
|
+
const classified = classifyTerragruntSource(source);
|
|
36
|
+
if (classified.pinned !== false) continue;
|
|
37
|
+
diagnostics.push({
|
|
38
|
+
checkId: "TF043",
|
|
39
|
+
severity: "warning",
|
|
40
|
+
message:
|
|
41
|
+
`The \`terraform\` block in ${block.file} sources "${source}" with no version, so a push or a release changes the unit with no diff to review. ` +
|
|
42
|
+
FIX[classified.kind],
|
|
43
|
+
entity: block.key,
|
|
44
|
+
lexicon: "terraform",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return diagnostics;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { loadTreeFixture } from "./fixtures/load";
|
|
3
|
+
import { tf044 } from "./tf044";
|
|
4
|
+
|
|
5
|
+
describe("TF044: Terragrunt config keeps state in a local backend", () => {
|
|
6
|
+
test("flags remote_state with backend = local", async () => {
|
|
7
|
+
const diags = tf044.check(await loadTreeFixture("TF044", "positive"));
|
|
8
|
+
expect(diags).toHaveLength(1);
|
|
9
|
+
expect(diags[0]).toMatchObject({ checkId: "TF044", severity: "warning", entity: "TF044/remote_state", lexicon: "terraform" });
|
|
10
|
+
expect(diags[0].message).toContain("remote_state");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("flags a generate block that writes a local backend", async () => {
|
|
14
|
+
const diags = tf044.check(await loadTreeFixture("TF044", "positive-generate"));
|
|
15
|
+
expect(diags).toHaveLength(1);
|
|
16
|
+
expect(diags[0].entity).toBe("TF044/generate.backend");
|
|
17
|
+
expect(diags[0].message).toContain('generate "backend"');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("an s3 backend passes", async () => {
|
|
21
|
+
expect(tf044.check(await loadTreeFixture("TF044", "negative"))).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("check metadata", () => {
|
|
25
|
+
expect(tf044.id).toBe("TF044");
|
|
26
|
+
});
|
|
27
|
+
});
|