@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
package/src/pin/scan.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Byte offsets for the attribute values the pin edit rewrites (#3189).
|
|
3
|
+
*
|
|
4
|
+
* `@cdktn/hcl2json` is the reader that says what a file means, and the pin
|
|
5
|
+
* edit asks it first (`./edit.ts`). It gives back values and no positions
|
|
6
|
+
* (`../hcl/value.ts` says why), so it cannot say WHERE in the file a
|
|
7
|
+
* `source` or `version` literal sits. This lexer answers only that: it walks
|
|
8
|
+
* the raw text token by token, skipping comments, quoted strings, template
|
|
9
|
+
* interpolations and heredocs, counts braces, and records each top-level
|
|
10
|
+
* block and the plain string literal of each of its own attributes, with the
|
|
11
|
+
* literal's offsets.
|
|
12
|
+
*
|
|
13
|
+
* It is an HCL tokenizer, not a pattern match over lines: a `source =` inside
|
|
14
|
+
* a comment, a heredoc or another string is never a match, and a block opened
|
|
15
|
+
* on the same line as its attributes reads the same as one split over lines.
|
|
16
|
+
* What it finds is checked against the reader before anything is written:
|
|
17
|
+
* the literal's unescaped value has to equal what hcl2json read for that
|
|
18
|
+
* attribute, and the edited file has to parse to the same tree with only the
|
|
19
|
+
* pinned values changed.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** One plain string literal: the offsets of its content, quotes excluded. */
|
|
23
|
+
export interface ScannedLiteral {
|
|
24
|
+
start: number;
|
|
25
|
+
end: number;
|
|
26
|
+
/** The content as written, escapes included. */
|
|
27
|
+
raw: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** One attribute of a block body. `literal` is set when the value is a single string with no interpolation. */
|
|
31
|
+
export interface ScannedAttribute {
|
|
32
|
+
name: string;
|
|
33
|
+
literal?: ScannedLiteral;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One top-level block: `module "vpc" { ... }`, `terraform { ... }`. */
|
|
37
|
+
export interface ScannedBlock {
|
|
38
|
+
type: string;
|
|
39
|
+
labels: string[];
|
|
40
|
+
/** Offset of the block's type keyword. */
|
|
41
|
+
start: number;
|
|
42
|
+
/** Offset just past the closing brace. */
|
|
43
|
+
end: number;
|
|
44
|
+
/** The block body's own attributes, in file order. Nested blocks' attributes are not included. */
|
|
45
|
+
attributes: ScannedAttribute[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type Token =
|
|
49
|
+
| { kind: "ident"; value: string; start: number; end: number }
|
|
50
|
+
| { kind: "string"; start: number; end: number; raw: string; template: boolean }
|
|
51
|
+
| { kind: "punct"; value: string; start: number; end: number }
|
|
52
|
+
| { kind: "newline"; start: number; end: number }
|
|
53
|
+
| { kind: "other"; start: number; end: number };
|
|
54
|
+
|
|
55
|
+
const IDENT_START = /[A-Za-z_]/;
|
|
56
|
+
const IDENT_PART = /[A-Za-z0-9_-]/;
|
|
57
|
+
|
|
58
|
+
/** Thrown when the text is not well-formed enough to place an edit in. */
|
|
59
|
+
export class HclScanError extends Error {
|
|
60
|
+
constructor(file: string, message: string) {
|
|
61
|
+
super(`${file}: ${message}`);
|
|
62
|
+
this.name = "HclScanError";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Skip a quoted string starting at `i` (the opening quote). Returns the index
|
|
68
|
+
* just past the closing quote, and whether a `${` or `%{` template appears.
|
|
69
|
+
*/
|
|
70
|
+
function skipString(text: string, i: number, file: string): { end: number; template: boolean } {
|
|
71
|
+
let j = i + 1;
|
|
72
|
+
let template = false;
|
|
73
|
+
while (j < text.length) {
|
|
74
|
+
const c = text[j];
|
|
75
|
+
if (c === "\\") {
|
|
76
|
+
j += 2;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (c === '"') return { end: j + 1, template };
|
|
80
|
+
if (c === "\n") throw new HclScanError(file, `unterminated string at offset ${i}`);
|
|
81
|
+
if ((c === "$" || c === "%") && text[j + 1] === "{") {
|
|
82
|
+
// `$${` and `%%{` are escapes for a literal `${` and `%{`.
|
|
83
|
+
if (text[j - 1] === c) {
|
|
84
|
+
j += 2;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
template = true;
|
|
88
|
+
j = skipTemplate(text, j + 2, file);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
j++;
|
|
92
|
+
}
|
|
93
|
+
throw new HclScanError(file, `unterminated string at offset ${i}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Skip a template interpolation body starting just inside `${`. Returns the index past its `}`. */
|
|
97
|
+
function skipTemplate(text: string, i: number, file: string): number {
|
|
98
|
+
let depth = 1;
|
|
99
|
+
let j = i;
|
|
100
|
+
while (j < text.length) {
|
|
101
|
+
const c = text[j];
|
|
102
|
+
if (c === '"') {
|
|
103
|
+
j = skipString(text, j, file).end;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (c === "{") depth++;
|
|
107
|
+
else if (c === "}") {
|
|
108
|
+
depth--;
|
|
109
|
+
if (depth === 0) return j + 1;
|
|
110
|
+
}
|
|
111
|
+
j++;
|
|
112
|
+
}
|
|
113
|
+
throw new HclScanError(file, `unterminated template at offset ${i}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function tokenize(text: string, file: string): Token[] {
|
|
117
|
+
const tokens: Token[] = [];
|
|
118
|
+
let i = 0;
|
|
119
|
+
while (i < text.length) {
|
|
120
|
+
const c = text[i]!;
|
|
121
|
+
if (c === "\n") {
|
|
122
|
+
tokens.push({ kind: "newline", start: i, end: i + 1 });
|
|
123
|
+
i++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (c === " " || c === "\t" || c === "\r") {
|
|
127
|
+
i++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (c === "#" || (c === "/" && text[i + 1] === "/")) {
|
|
131
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (c === "/" && text[i + 1] === "*") {
|
|
135
|
+
const close = text.indexOf("*/", i + 2);
|
|
136
|
+
if (close < 0) throw new HclScanError(file, `unterminated comment at offset ${i}`);
|
|
137
|
+
// A block comment spanning lines still ends a statement where a newline would.
|
|
138
|
+
if (text.slice(i, close).includes("\n")) tokens.push({ kind: "newline", start: i, end: close + 2 });
|
|
139
|
+
i = close + 2;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (c === '"') {
|
|
143
|
+
const { end, template } = skipString(text, i, file);
|
|
144
|
+
tokens.push({ kind: "string", start: i, end, raw: text.slice(i + 1, end - 1), template });
|
|
145
|
+
i = end;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (c === "<" && text[i + 1] === "<" && /[-A-Za-z_]/.test(text[i + 2] ?? "")) {
|
|
149
|
+
const m = /^<<-?([A-Za-z_][A-Za-z0-9_-]*)[ \t]*\r?\n/.exec(text.slice(i));
|
|
150
|
+
if (m) {
|
|
151
|
+
const marker = m[1]!;
|
|
152
|
+
let j = i + m[0].length;
|
|
153
|
+
for (;;) {
|
|
154
|
+
const lineEnd = text.indexOf("\n", j);
|
|
155
|
+
const line = text.slice(j, lineEnd < 0 ? text.length : lineEnd);
|
|
156
|
+
if (line.trim() === marker) {
|
|
157
|
+
j = lineEnd < 0 ? text.length : lineEnd;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
if (lineEnd < 0) throw new HclScanError(file, `unterminated heredoc ${marker} at offset ${i}`);
|
|
161
|
+
j = lineEnd + 1;
|
|
162
|
+
}
|
|
163
|
+
tokens.push({ kind: "other", start: i, end: j });
|
|
164
|
+
i = j;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (IDENT_START.test(c)) {
|
|
169
|
+
let j = i + 1;
|
|
170
|
+
while (j < text.length && IDENT_PART.test(text[j]!)) j++;
|
|
171
|
+
tokens.push({ kind: "ident", value: text.slice(i, j), start: i, end: j });
|
|
172
|
+
i = j;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const two = text.slice(i, i + 2);
|
|
176
|
+
if (two === "==" || two === "!=" || two === "<=" || two === ">=" || two === "=>" || two === "&&" || two === "||") {
|
|
177
|
+
tokens.push({ kind: "punct", value: two, start: i, end: i + 2 });
|
|
178
|
+
i += 2;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if ("{}[]()=,.:?".includes(c)) {
|
|
182
|
+
tokens.push({ kind: "punct", value: c, start: i, end: i + 1 });
|
|
183
|
+
i++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
tokens.push({ kind: "other", start: i, end: i + 1 });
|
|
187
|
+
i++;
|
|
188
|
+
}
|
|
189
|
+
return tokens;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const OPEN = new Set(["{", "[", "("]);
|
|
193
|
+
const CLOSE = new Set(["}", "]", ")"]);
|
|
194
|
+
|
|
195
|
+
/** Every top-level block in `text`, with its own attributes' literals. */
|
|
196
|
+
export function scanBlocks(text: string, file: string): ScannedBlock[] {
|
|
197
|
+
const tokens = tokenize(text, file);
|
|
198
|
+
const blocks: ScannedBlock[] = [];
|
|
199
|
+
let i = 0;
|
|
200
|
+
const atStatementStart = (k: number) => k === 0 || tokens[k - 1]!.kind === "newline" || (tokens[k - 1]!.kind === "punct" && (tokens[k - 1] as { value: string }).value === "{");
|
|
201
|
+
|
|
202
|
+
while (i < tokens.length) {
|
|
203
|
+
const t = tokens[i]!;
|
|
204
|
+
if (t.kind !== "ident" || !atStatementStart(i)) {
|
|
205
|
+
i = skipStatement(tokens, i);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// A block header: a type, then string or identifier labels, then `{`.
|
|
209
|
+
const labels: string[] = [];
|
|
210
|
+
let j = i + 1;
|
|
211
|
+
while (j < tokens.length) {
|
|
212
|
+
const l = tokens[j]!;
|
|
213
|
+
if (l.kind === "string" && !l.template) labels.push(l.raw);
|
|
214
|
+
else if (l.kind === "ident") labels.push(l.value);
|
|
215
|
+
else break;
|
|
216
|
+
j++;
|
|
217
|
+
}
|
|
218
|
+
const brace = tokens[j];
|
|
219
|
+
if (!brace || brace.kind !== "punct" || brace.value !== "{") {
|
|
220
|
+
i = skipStatement(tokens, i);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const { end, attributes } = readBody(tokens, j, file);
|
|
224
|
+
blocks.push({ type: t.value, labels, start: t.start, end: tokens[end]!.end, attributes });
|
|
225
|
+
i = end + 1;
|
|
226
|
+
}
|
|
227
|
+
return blocks;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Skip to the token after the end of the statement starting at `i`: the next newline at bracket depth 0. */
|
|
231
|
+
function skipStatement(tokens: Token[], i: number): number {
|
|
232
|
+
let depth = 0;
|
|
233
|
+
let j = i;
|
|
234
|
+
while (j < tokens.length) {
|
|
235
|
+
const t = tokens[j]!;
|
|
236
|
+
if (t.kind === "punct" && OPEN.has(t.value)) depth++;
|
|
237
|
+
else if (t.kind === "punct" && CLOSE.has(t.value)) depth = Math.max(0, depth - 1);
|
|
238
|
+
else if (t.kind === "newline" && depth === 0) return j + 1;
|
|
239
|
+
j++;
|
|
240
|
+
}
|
|
241
|
+
return j;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Read a block body whose `{` is at `open`. Returns the index of the matching `}` and the body's own attributes. */
|
|
245
|
+
function readBody(tokens: Token[], open: number, file: string): { end: number; attributes: ScannedAttribute[] } {
|
|
246
|
+
const attributes: ScannedAttribute[] = [];
|
|
247
|
+
let depth = 1;
|
|
248
|
+
let j = open + 1;
|
|
249
|
+
let statementStart = true;
|
|
250
|
+
while (j < tokens.length) {
|
|
251
|
+
const t = tokens[j]!;
|
|
252
|
+
if (t.kind === "newline") {
|
|
253
|
+
if (depth === 1) statementStart = true;
|
|
254
|
+
j++;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (depth === 1 && statementStart && t.kind === "ident") {
|
|
258
|
+
const eq = tokens[j + 1];
|
|
259
|
+
if (eq && eq.kind === "punct" && eq.value === "=") {
|
|
260
|
+
const value = tokens[j + 2];
|
|
261
|
+
const after = tokens[j + 3];
|
|
262
|
+
const ends = !after || after.kind === "newline" || (after.kind === "punct" && after.value === "}");
|
|
263
|
+
const literal = value && value.kind === "string" && !value.template && ends ? { start: value.start + 1, end: value.end - 1, raw: value.raw } : undefined;
|
|
264
|
+
attributes.push({ name: t.value, ...(literal ? { literal } : {}) });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
statementStart = false;
|
|
268
|
+
if (t.kind === "punct" && OPEN.has(t.value)) depth++;
|
|
269
|
+
else if (t.kind === "punct" && CLOSE.has(t.value)) {
|
|
270
|
+
depth--;
|
|
271
|
+
if (depth === 0) return { end: j, attributes };
|
|
272
|
+
}
|
|
273
|
+
j++;
|
|
274
|
+
}
|
|
275
|
+
throw new HclScanError(file, `unbalanced braces: a block opened at offset ${tokens[open]!.start} never closes`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Undo HCL's string escapes, so a literal's raw text compares to the value the reader returned. */
|
|
279
|
+
export function unescapeHcl(raw: string): string {
|
|
280
|
+
let out = "";
|
|
281
|
+
for (let i = 0; i < raw.length; i++) {
|
|
282
|
+
const c = raw[i]!;
|
|
283
|
+
if (c === "\\") {
|
|
284
|
+
const n = raw[i + 1];
|
|
285
|
+
i++;
|
|
286
|
+
if (n === "n") out += "\n";
|
|
287
|
+
else if (n === "t") out += "\t";
|
|
288
|
+
else if (n === "r") out += "\r";
|
|
289
|
+
else if (n === "u" || n === "U") {
|
|
290
|
+
const len = n === "u" ? 4 : 8;
|
|
291
|
+
out += String.fromCodePoint(parseInt(raw.slice(i + 1, i + 1 + len), 16));
|
|
292
|
+
i += len;
|
|
293
|
+
} else out += n ?? "";
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if ((c === "$" || c === "%") && raw[i + 1] === c && raw[i + 2] === "{") {
|
|
297
|
+
out += `${c}{`;
|
|
298
|
+
i += 2;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
out += c;
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Write a value as the content of an HCL string literal. */
|
|
307
|
+
export function escapeHcl(value: string): string {
|
|
308
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\$\{/g, "$${").replace(/%\{/g, "%%{");
|
|
309
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a module call keeps its pin, and how the pin moves (#3189).
|
|
3
|
+
*
|
|
4
|
+
* A module call names a module and, if it is pinned, one exact version of it.
|
|
5
|
+
* The version sits in one of two places:
|
|
6
|
+
*
|
|
7
|
+
* - the `version` argument, beside a registry `source`
|
|
8
|
+
* (`source = "app.terraform.io/acme/vpc/aws"`, `version = "1.3.0"`);
|
|
9
|
+
* - a query argument of the `source` itself: OpenTofu's `oci://` sources take
|
|
10
|
+
* `?tag=` or `?digest=`, a git source takes `?ref=`, and Terragrunt's
|
|
11
|
+
* `tfr://` source takes `?version=`.
|
|
12
|
+
*
|
|
13
|
+
* The module's identity is the source with the pin taken out, so
|
|
14
|
+
* `oci://r.example.com/m/vpc?tag=1.3.0` and `oci://r.example.com/m/vpc?tag=1.4.0`
|
|
15
|
+
* call the same module.
|
|
16
|
+
*
|
|
17
|
+
* A `version` constraint (`~> 1.4`, `>= 1.4, < 2`) is not a pin. An upstream
|
|
18
|
+
* release changes what such a root runs with no diff to review, and there is
|
|
19
|
+
* no single version to move, so the call is refused with that reason. The
|
|
20
|
+
* same holds for a call with no pin at all.
|
|
21
|
+
*
|
|
22
|
+
* OpenTofu writes an OCI pin as a query argument
|
|
23
|
+
* (https://opentofu.org/docs/language/modules/sources/#oci-distribution-registries),
|
|
24
|
+
* not as a `:tag` or `@sha256:` suffix on the repository, so the query form is
|
|
25
|
+
* the one read here.
|
|
26
|
+
*
|
|
27
|
+
* This module is pure string work over values the HCL reader has already
|
|
28
|
+
* parsed. Finding those values in a file, and writing the new ones back, is
|
|
29
|
+
* `./edit.ts`. #3190 adds an `oci` kind to the lint classifier
|
|
30
|
+
* (`../lint/post-synth/module-source.ts`) with the same parse; the two can
|
|
31
|
+
* share one once both have landed.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** The query arguments that carry a pin. */
|
|
35
|
+
export type PinParam = "tag" | "digest" | "ref" | "version";
|
|
36
|
+
|
|
37
|
+
/** What a module call pins, read from its `source` and `version`. */
|
|
38
|
+
export interface ModuleCallPin {
|
|
39
|
+
/** The module the call names: its `source` with the pin argument taken out. */
|
|
40
|
+
module: string;
|
|
41
|
+
/** Where the pin is: the `version` argument, or a query argument of `source`. */
|
|
42
|
+
at: "version" | "source";
|
|
43
|
+
/** The query argument holding the pin, when `at` is `source`. */
|
|
44
|
+
param?: PinParam;
|
|
45
|
+
/** The pinned version, tag, digest or ref. Null when there is no pin to move. */
|
|
46
|
+
pin: string | null;
|
|
47
|
+
/** Why the call has no pin to move, when `pin` is null. */
|
|
48
|
+
unpinned?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The outcome of moving one call's pin. */
|
|
52
|
+
export type PinMove =
|
|
53
|
+
| { outcome: "moved"; from: string; to: string; source: string; version?: string }
|
|
54
|
+
| { outcome: "already"; pin: string }
|
|
55
|
+
| { outcome: "elsewhere"; pin: string }
|
|
56
|
+
| { outcome: "refused"; reason: string };
|
|
57
|
+
|
|
58
|
+
/** `1.4.0`, `v1.4.0`, `1.4`, `1.4.0-rc.1+build.5`: one exact version. */
|
|
59
|
+
const EXACT_VERSION = /^v?\d+(\.\d+){0,2}(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
|
|
60
|
+
|
|
61
|
+
/** An OCI digest, `sha256:<hex>`. */
|
|
62
|
+
const DIGEST = /^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[0-9a-fA-F]{32,}$/;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The exact version a `version` argument pins, or null when it is a
|
|
66
|
+
* constraint. `1.4.0` and `= 1.4.0` are exact; `~> 1.4`, `>= 1.4` and
|
|
67
|
+
* `1.4.0, < 2` are not.
|
|
68
|
+
*/
|
|
69
|
+
export function exactVersion(constraint: string): string | null {
|
|
70
|
+
const trimmed = constraint.trim().replace(/^=\s*/, "");
|
|
71
|
+
return EXACT_VERSION.test(trimmed) ? trimmed : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Whether a value can be a pin's new value: an exact version, a tag, a ref or a digest, never a constraint. */
|
|
75
|
+
export function isPinValue(value: string): boolean {
|
|
76
|
+
return value.length > 0 && !/[\s,<>~!=*^]/.test(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface SplitSource {
|
|
80
|
+
base: string;
|
|
81
|
+
params: Array<{ key: string; value: string | undefined; raw: string }>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function splitSource(source: string): SplitSource {
|
|
85
|
+
const q = source.indexOf("?");
|
|
86
|
+
if (q < 0) return { base: source, params: [] };
|
|
87
|
+
const params = source
|
|
88
|
+
.slice(q + 1)
|
|
89
|
+
.split("&")
|
|
90
|
+
.filter((raw) => raw.length > 0)
|
|
91
|
+
.map((raw) => {
|
|
92
|
+
const eq = raw.indexOf("=");
|
|
93
|
+
return eq < 0 ? { key: raw, value: undefined, raw } : { key: raw.slice(0, eq), value: decode(raw.slice(eq + 1)), raw };
|
|
94
|
+
});
|
|
95
|
+
return { base: source.slice(0, q), params };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function decode(value: string): string {
|
|
99
|
+
try {
|
|
100
|
+
return decodeURIComponent(value);
|
|
101
|
+
} catch {
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function joinSource(base: string, raws: string[]): string {
|
|
107
|
+
return raws.length === 0 ? base : `${base}?${raws.join("&")}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isLocal(source: string): boolean {
|
|
111
|
+
return source.startsWith("./") || source.startsWith("../");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isOci(source: string): boolean {
|
|
115
|
+
return source.startsWith("oci://");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isGit(source: string): boolean {
|
|
119
|
+
return source.startsWith("git::") || source.startsWith("git@") || /^(github\.com|bitbucket\.org)\//.test(source) || /\.git(\/\/|\?|$)/.test(source);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Read the pin a module call carries. `version` is the call's `version` argument, when it has one. */
|
|
123
|
+
export function readModulePin(source: string, version?: string): ModuleCallPin {
|
|
124
|
+
const call = readPin(source, version);
|
|
125
|
+
const written = call.at === "version" ? version! : call.pin;
|
|
126
|
+
if (written != null && /[$%]\{/.test(written)) {
|
|
127
|
+
return { ...call, pin: null, unpinned: `the pin "${written}" is an expression, so there is no literal version to move` };
|
|
128
|
+
}
|
|
129
|
+
return call;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function readPin(source: string, version?: string): ModuleCallPin {
|
|
133
|
+
if (version !== undefined) {
|
|
134
|
+
const exact = exactVersion(version);
|
|
135
|
+
return exact
|
|
136
|
+
? { module: source, at: "version", pin: exact }
|
|
137
|
+
: { module: source, at: "version", pin: null, unpinned: `version "${version}" is a constraint, not a pin: there is no one version to move` };
|
|
138
|
+
}
|
|
139
|
+
const { base, params } = splitSource(source);
|
|
140
|
+
const keep = (taken: string) => joinSource(base, params.filter((p) => p.key !== taken).map((p) => p.raw));
|
|
141
|
+
const find = (key: PinParam) => params.find((p) => p.key === key);
|
|
142
|
+
|
|
143
|
+
if (isOci(source)) {
|
|
144
|
+
const tag = find("tag");
|
|
145
|
+
const digest = find("digest");
|
|
146
|
+
if (tag && digest) return { module: joinSource(base, params.filter((p) => p.key !== "tag" && p.key !== "digest").map((p) => p.raw)), at: "source", pin: null, unpinned: "the oci source names both a tag and a digest" };
|
|
147
|
+
const p = digest ?? tag;
|
|
148
|
+
if (!p || !p.value) return { module: source, at: "source", pin: null, unpinned: "the oci source names no tag or digest, so it resolves whatever the registry calls latest" };
|
|
149
|
+
return { module: keep(p.key), at: "source", param: p.key as PinParam, pin: p.value };
|
|
150
|
+
}
|
|
151
|
+
const ref = find("ref");
|
|
152
|
+
if (ref?.value) return { module: keep("ref"), at: "source", param: "ref", pin: ref.value };
|
|
153
|
+
const ver = find("version");
|
|
154
|
+
if (ver?.value) {
|
|
155
|
+
const exact = exactVersion(ver.value);
|
|
156
|
+
return exact
|
|
157
|
+
? { module: keep("version"), at: "source", param: "version", pin: exact }
|
|
158
|
+
: { module: keep("version"), at: "source", param: "version", pin: null, unpinned: `version "${ver.value}" is a constraint, not a pin: there is no one version to move` };
|
|
159
|
+
}
|
|
160
|
+
if (isLocal(source)) return { module: source, at: "source", pin: null, unpinned: "a local path has no version to pin" };
|
|
161
|
+
if (isGit(source)) return { module: source, at: "source", pin: null, unpinned: "the git source names no ref, so it follows the default branch" };
|
|
162
|
+
return { module: source, at: "source", pin: null, unpinned: "the source has no version argument and no pin in its address, so it resolves the newest release" };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Replace one query argument's value, keeping every other byte of the source. */
|
|
166
|
+
function withParam(source: string, from: PinParam, to: PinParam, value: string): string {
|
|
167
|
+
const { base, params } = splitSource(source);
|
|
168
|
+
return joinSource(
|
|
169
|
+
base,
|
|
170
|
+
params.map((p) => (p.key === from ? `${to}=${value}` : p.raw)),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Move a call's pin from `from` to `to`, when the call names `module`.
|
|
176
|
+
* Returns null when it names another module. `version` keeps any `=` it was
|
|
177
|
+
* written with, so `= 1.3.0` becomes `= 1.4.0`.
|
|
178
|
+
*/
|
|
179
|
+
export function movePin(source: string, version: string | undefined, request: { module: string; from: string; to: string }): PinMove | null {
|
|
180
|
+
const call = readModulePin(source, version);
|
|
181
|
+
if (call.module !== request.module) return null;
|
|
182
|
+
if (call.pin === null) return { outcome: "refused", reason: call.unpinned ?? "no pin" };
|
|
183
|
+
if (call.pin === request.to) return { outcome: "already", pin: call.pin };
|
|
184
|
+
if (call.pin !== request.from) return { outcome: "elsewhere", pin: call.pin };
|
|
185
|
+
if (call.at === "version") {
|
|
186
|
+
const at = version!.lastIndexOf(call.pin);
|
|
187
|
+
return { outcome: "moved", from: request.from, to: request.to, source, version: version!.slice(0, at) + request.to + version!.slice(at + call.pin.length) };
|
|
188
|
+
}
|
|
189
|
+
// An oci pin moving between a tag and a digest changes which argument holds it.
|
|
190
|
+
const param: PinParam = call.param === "tag" || call.param === "digest" ? (DIGEST.test(request.to) ? "digest" : "tag") : call.param!;
|
|
191
|
+
return { outcome: "moved", from: request.from, to: request.to, source: withParam(source, call.param!, param, request.to) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The module a source names, with any pin taken out, so a caller may name the module by a pinned source. */
|
|
195
|
+
export function moduleOf(source: string): string {
|
|
196
|
+
return readModulePin(source).module;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Refuse a request whose `to` is not something a pin can hold. */
|
|
200
|
+
export function checkPinRequest(request: { module: string; from: string; to: string }): void {
|
|
201
|
+
for (const [name, value] of [["from", request.from], ["to", request.to]] as const) {
|
|
202
|
+
if (!isPinValue(value)) throw new Error(`pin ${name} "${value}" is not an exact version, tag, ref or digest`);
|
|
203
|
+
}
|
|
204
|
+
if (request.from === request.to) throw new Error(`pin from and to are both "${request.to}": there is nothing to move`);
|
|
205
|
+
if (request.module.length === 0) throw new Error("pin module is empty: name the module's source without its pin");
|
|
206
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import { loadHcl2json } from "@intentius/chant/terraform/hcl2json";
|
|
4
|
+
import { planPinWaves, restrictWaves, terragruntDependencies, wavesFromChoudoufu, PinWavePlanError } from "./waves";
|
|
5
|
+
|
|
6
|
+
/** choudoufu#1750's N=5 shape: e02 reads e01, e03 reads e02; e04 and e05 read nothing. */
|
|
7
|
+
const N5 = [
|
|
8
|
+
{ root: "estates/e01" },
|
|
9
|
+
{ root: "estates/e02", dependsOn: ["estates/e01"] },
|
|
10
|
+
{ root: "estates/e03", dependsOn: ["estates/e02"] },
|
|
11
|
+
{ root: "estates/e04" },
|
|
12
|
+
{ root: "estates/e05" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
describe("planPinWaves (#3189)", () => {
|
|
16
|
+
test("a canary list forms wave 1, and dependency order forms the rest", () => {
|
|
17
|
+
expect(planPinWaves(N5, ["estates/e04"])).toEqual([
|
|
18
|
+
{ wave: 1, canary: true, roots: ["estates/e04"] },
|
|
19
|
+
{ wave: 2, canary: false, roots: ["estates/e01", "estates/e05"] },
|
|
20
|
+
{ wave: 3, canary: false, roots: ["estates/e02"] },
|
|
21
|
+
{ wave: 4, canary: false, roots: ["estates/e03"] },
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("no reader lands in a wave before what it reads", () => {
|
|
26
|
+
const waves = planPinWaves(N5);
|
|
27
|
+
const waveOf = new Map(waves.flatMap((w) => w.roots.map((r) => [r, w.wave] as const)));
|
|
28
|
+
for (const r of N5) for (const d of r.dependsOn ?? []) expect(waveOf.get(d)!, `${r.root} after ${d}`).toBeLessThan(waveOf.get(r.root)!);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("the canary wave whatever the graph order, even a root others read", () => {
|
|
32
|
+
expect(planPinWaves(N5, ["estates/e01"]).map((w) => w.roots)).toEqual([["estates/e01"], ["estates/e02", "estates/e04", "estates/e05"], ["estates/e03"]]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("a canary that reads a root outside the canaries is refused", () => {
|
|
36
|
+
expect(() => planPinWaves(N5, ["estates/e03"])).toThrow(/canary estates\/e03 depends on estates\/e02, which is not a canary/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("a canary that is not a root, and a cycle, are refused by name", () => {
|
|
40
|
+
expect(() => planPinWaves(N5, ["estates/e09"])).toThrow(PinWavePlanError);
|
|
41
|
+
expect(() =>
|
|
42
|
+
planPinWaves([
|
|
43
|
+
{ root: "a", dependsOn: ["b"] },
|
|
44
|
+
{ root: "b", dependsOn: ["a"] },
|
|
45
|
+
]),
|
|
46
|
+
).toThrow(/dependency cycle among roots: a, b/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("a dependency outside the rollout orders nothing", () => {
|
|
50
|
+
expect(planPinWaves([{ root: "a", dependsOn: ["elsewhere"] }])).toEqual([{ wave: 1, canary: false, roots: ["a"] }]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("wavesFromChoudoufu", () => {
|
|
55
|
+
// `choudoufu live-waves -json -canary=estates/e04` over choudoufu#1750's N=5 fixture, recorded from choudoufu main.
|
|
56
|
+
const doc = JSON.parse(readFileSync(new URL("../__fixtures__/pin/choudoufu-live-waves-n5.json", import.meta.url), "utf-8"));
|
|
57
|
+
|
|
58
|
+
test("takes each wave's root list as choudoufu's planner emits it", () => {
|
|
59
|
+
expect(wavesFromChoudoufu(doc)).toEqual([
|
|
60
|
+
{ wave: 1, canary: true, roots: ["estates/e04"] },
|
|
61
|
+
{ wave: 2, canary: false, roots: ["estates/e01", "estates/e05"] },
|
|
62
|
+
{ wave: 3, canary: false, roots: ["estates/e02"] },
|
|
63
|
+
{ wave: 4, canary: false, roots: ["estates/e03"] },
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("agrees with the declared-order planner on the same graph", () => {
|
|
68
|
+
expect(wavesFromChoudoufu(doc)).toEqual(planPinWaves(N5, ["estates/e04"]));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("a prefix places the roots under the directory choudoufu ran in", () => {
|
|
72
|
+
expect(wavesFromChoudoufu(doc, "infra")[0]!.roots).toEqual(["infra/estates/e04"]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("refuses another format", () => {
|
|
76
|
+
expect(() => wavesFromChoudoufu({ format_version: "2", waves: [] })).toThrow(/format version 1/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("restrictWaves drops roots not moving and renumbers", () => {
|
|
80
|
+
expect(restrictWaves(wavesFromChoudoufu(doc), new Set(["estates/e02", "estates/e05"]))).toEqual([
|
|
81
|
+
{ wave: 1, canary: false, roots: ["estates/e05"] },
|
|
82
|
+
{ wave: 2, canary: false, roots: ["estates/e02"] },
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("terragruntDependencies", () => {
|
|
88
|
+
test("reads dependency config_path and dependencies paths, relative to the unit", async () => {
|
|
89
|
+
const parser = await loadHcl2json();
|
|
90
|
+
const text = [
|
|
91
|
+
`dependency "vpc" {`,
|
|
92
|
+
` config_path = "../vpc"`,
|
|
93
|
+
` mock_outputs = { id = "x" }`,
|
|
94
|
+
`}`,
|
|
95
|
+
`dependencies {`,
|
|
96
|
+
` paths = ["../dns", "\${get_repo_root()}/x"]`,
|
|
97
|
+
`}`,
|
|
98
|
+
].join("\n");
|
|
99
|
+
expect(await terragruntDependencies("live/prod/app", text, parser)).toEqual(["live/prod/dns", "live/prod/vpc"]);
|
|
100
|
+
});
|
|
101
|
+
});
|