@intentius/chant-lexicon-terraform 0.124.31 → 0.124.33
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.js +111 -0
- package/dist/__fixtures__/terragrunt/record.js +57 -0
- package/dist/behaviour/index.js +6 -0
- package/dist/behaviour/kinds.js +194 -0
- package/dist/behaviour/predict.js +297 -0
- package/dist/behaviour/reference-catalog.js +178 -0
- package/dist/behaviour/request.js +482 -0
- package/dist/change-set.js +234 -0
- package/dist/changed-roots.js +97 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +44 -0
- package/dist/codegen/generate-cli.js +9 -0
- package/dist/codegen/generate.js +59 -0
- package/dist/codegen/package.js +43 -0
- package/dist/commands.js +106 -0
- package/dist/components/capability-plugin.d.ts +38 -2
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/capability-plugin.js +260 -0
- package/dist/composites/terraform-adopt-op.d.ts +2 -0
- package/dist/composites/terraform-adopt-op.d.ts.map +1 -1
- package/dist/composites/terraform-adopt-op.js +153 -0
- package/dist/composites/terraform-apply-op.d.ts +2 -0
- package/dist/composites/terraform-apply-op.d.ts.map +1 -1
- package/dist/composites/terraform-apply-op.js +184 -0
- package/dist/composites/terraform-pin-rollout-op.js +45 -0
- package/dist/composites/terraform-watch-op.js +219 -0
- package/dist/config.js +118 -0
- package/dist/describe-resources.js +984 -0
- package/dist/generated/index.js +1 -0
- package/dist/hcl/descend.js +208 -0
- package/dist/hcl/edges.js +287 -0
- package/dist/hcl/jsonencode.js +442 -0
- package/dist/hcl/marker.js +212 -0
- package/dist/hcl/parse.js +372 -0
- package/dist/hcl/references.js +156 -0
- package/dist/hcl/roots.js +98 -0
- package/dist/hcl/suppressions.js +189 -0
- package/dist/hcl/value.js +72 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +501 -0
- package/dist/lint/audit-lineage.js +442 -0
- package/dist/lint/post-synth/blocks.js +87 -0
- package/dist/lint/post-synth/fixtures/load.js +94 -0
- package/dist/lint/post-synth/flat-attributes.js +135 -0
- package/dist/lint/post-synth/index.js +90 -0
- package/dist/lint/post-synth/module-source.js +153 -0
- package/dist/lint/post-synth/scope.js +104 -0
- package/dist/lint/post-synth/terragrunt.js +63 -0
- package/dist/lint/post-synth/tf001.js +138 -0
- package/dist/lint/post-synth/tf002.js +143 -0
- package/dist/lint/post-synth/tf003.js +60 -0
- package/dist/lint/post-synth/tf004.js +54 -0
- package/dist/lint/post-synth/tf005.js +68 -0
- package/dist/lint/post-synth/tf006.js +43 -0
- package/dist/lint/post-synth/tf007.js +66 -0
- package/dist/lint/post-synth/tf008.js +64 -0
- package/dist/lint/post-synth/tf009.js +49 -0
- package/dist/lint/post-synth/tf010.js +40 -0
- package/dist/lint/post-synth/tf011.js +38 -0
- package/dist/lint/post-synth/tf012.js +36 -0
- package/dist/lint/post-synth/tf013.js +53 -0
- package/dist/lint/post-synth/tf014.js +61 -0
- package/dist/lint/post-synth/tf015.js +62 -0
- package/dist/lint/post-synth/tf016.js +66 -0
- package/dist/lint/post-synth/tf017.js +39 -0
- package/dist/lint/post-synth/tf018.js +63 -0
- package/dist/lint/post-synth/tf019.js +60 -0
- package/dist/lint/post-synth/tf020.js +103 -0
- package/dist/lint/post-synth/tf021.js +117 -0
- package/dist/lint/post-synth/tf022.js +51 -0
- package/dist/lint/post-synth/tf024.js +62 -0
- package/dist/lint/post-synth/tf025.js +83 -0
- package/dist/lint/post-synth/tf026.js +74 -0
- package/dist/lint/post-synth/tf027.js +66 -0
- package/dist/lint/post-synth/tf028.js +83 -0
- package/dist/lint/post-synth/tf029.js +73 -0
- package/dist/lint/post-synth/tf030.js +253 -0
- package/dist/lint/post-synth/tf031.js +332 -0
- package/dist/lint/post-synth/tf032.js +201 -0
- package/dist/lint/post-synth/tf033.js +126 -0
- package/dist/lint/post-synth/tf034.js +45 -0
- package/dist/lint/post-synth/tf035.js +63 -0
- package/dist/lint/post-synth/tf036.js +97 -0
- package/dist/lint/post-synth/tf037.js +80 -0
- package/dist/lint/post-synth/tf038.js +63 -0
- package/dist/lint/post-synth/tf039.js +55 -0
- package/dist/lint/post-synth/tf040.js +67 -0
- package/dist/lint/post-synth/tf041.js +53 -0
- package/dist/lint/post-synth/tf042.js +38 -0
- package/dist/lint/post-synth/tf043.js +45 -0
- package/dist/lint/post-synth/tf044.js +45 -0
- package/dist/lint/post-synth/tf045.js +44 -0
- package/dist/lint/rules/fixtures/load.js +50 -0
- package/dist/lint/rules/index.js +4 -0
- package/dist/lint/rules/plan-before-apply.js +136 -0
- package/dist/lint/secret-shape.js +149 -0
- package/dist/live-ownership.js +57 -0
- package/dist/lsp/completions.js +95 -0
- package/dist/lsp/context.js +358 -0
- package/dist/lsp/hover.js +45 -0
- package/dist/lsp/option-keys.js +93 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/index.js +30 -0
- package/dist/op/activities/live-detect.js +143 -0
- package/dist/op/activities/pin-rollout.js +45 -0
- package/dist/op/activities/terraform.js +1270 -0
- package/dist/op/activity-contracts.js +162 -0
- package/dist/op/adoption.js +272 -0
- package/dist/op/builders.js +125 -0
- package/dist/op/resolve-root-mode.js +77 -0
- package/dist/package-cli.js +16 -0
- package/dist/pin/edit-ts.js +76 -0
- package/dist/pin/edit.js +118 -0
- package/dist/pin/forge.js +65 -0
- package/dist/pin/index.js +16 -0
- package/dist/pin/rollout.d.ts.map +1 -1
- package/dist/pin/rollout.js +382 -0
- package/dist/pin/scan.js +287 -0
- package/dist/pin/source.js +171 -0
- package/dist/pin/waves.js +109 -0
- package/dist/plan-digest.js +67 -0
- package/dist/plugin.js +323 -0
- package/dist/serializer.js +23 -0
- package/dist/state-ownership.js +29 -0
- package/dist/terragrunt/affected.d.ts.map +1 -1
- package/dist/terragrunt/affected.js +290 -0
- package/dist/terragrunt/index.js +10 -0
- package/dist/terragrunt/mocks.js +270 -0
- package/dist/terragrunt/run.js +264 -0
- package/dist/terragrunt/units.js +195 -0
- package/dist/terragrunt/wave.js +125 -0
- package/dist/validate-cli.js +4 -0
- package/dist/validate.js +31 -0
- package/package.json +10 -10
- package/src/codegen/package.ts +2 -2
- package/src/components/capability-plugin.test.ts +97 -2
- package/src/components/capability-plugin.ts +117 -12
- package/src/components/pr-loop.tofu.test.ts +51 -11
- package/src/composites/terraform-adopt-op.test.ts +6 -0
- package/src/composites/terraform-adopt-op.ts +7 -2
- package/src/composites/terraform-apply-op.test.ts +6 -0
- package/src/composites/terraform-apply-op.ts +7 -2
- package/src/index.ts +1 -0
- package/src/pin/rollout.ts +2 -1
- package/src/terragrunt/affected.test.ts +11 -0
- package/src/terragrunt/affected.ts +2 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* terraform serializer.
|
|
3
|
+
*
|
|
4
|
+
* A stub, deliberately. The entities this lexicon carries come from `.tf`
|
|
5
|
+
* files that already exist on disk (`buildRoots()` parses the roots named in
|
|
6
|
+
* `terraform.roots`), so there is nothing for chant to write back: emitting
|
|
7
|
+
* HCL here would put a second, generated copy of the estate beside the
|
|
8
|
+
* authored one. The lexicon's value is what the entities let the rest of
|
|
9
|
+
* chant do with the root (post-synth checks, `chant audit`, the Op surface),
|
|
10
|
+
* not a rendered artifact.
|
|
11
|
+
*
|
|
12
|
+
* `name` and `rulePrefix` are the two members the `Serializer` contract
|
|
13
|
+
* requires, and both are load-bearing: `name` is the key `build()` files this
|
|
14
|
+
* lexicon's output under, and `rulePrefix` is what every `TF*` id is checked
|
|
15
|
+
* against.
|
|
16
|
+
*/
|
|
17
|
+
export const terraformSerializer = {
|
|
18
|
+
name: "terraform",
|
|
19
|
+
rulePrefix: "TF",
|
|
20
|
+
serialize(_entities) {
|
|
21
|
+
return "";
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terraform's ownership channel (#2087): the state file.
|
|
3
|
+
*
|
|
4
|
+
* Its own module, and a tiny one, for the same reason cedar's
|
|
5
|
+
* `src/avp/ownership.ts` is: `plugin.ts` declares `ownershipChannel` eagerly
|
|
6
|
+
* while `describeResources` is behind a dynamic import, so the keys cannot
|
|
7
|
+
* live in the reader without dragging the reader (and `node:child_process`
|
|
8
|
+
* with it) into every plugin load.
|
|
9
|
+
*
|
|
10
|
+
* The design record is the module doc of `./describe-resources.ts` and the
|
|
11
|
+
* reader-facing version is `docs/pages/observation.mdx`. The short form:
|
|
12
|
+
* chant elsewhere answers "is this mine?" from a marker it stamped onto the
|
|
13
|
+
* live resource; terraform answers it from the state file, which already
|
|
14
|
+
* records exactly which addresses a configuration manages. Nothing is ever
|
|
15
|
+
* stamped, so nothing is ever written back over an estate's own tags.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Where the verdict is read from, rather than what is written.
|
|
19
|
+
*
|
|
20
|
+
* {@link ChannelKeys} names tag or label keys everywhere else, because
|
|
21
|
+
* everywhere else the marker is a key/value pair on the resource. These three
|
|
22
|
+
* name the state file, the configured root that owns that state, and the
|
|
23
|
+
* workspace the root selects.
|
|
24
|
+
*/
|
|
25
|
+
export const TERRAFORM_STATE_OWNERSHIP_KEYS = {
|
|
26
|
+
managedBy: "terraform.state",
|
|
27
|
+
stack: "terraform.root",
|
|
28
|
+
env: "terraform.workspace",
|
|
29
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"affected.d.ts","sourceRoot":"","sources":["../../src/terragrunt/affected.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAKH,OAAO,EAAqE,KAAK,qBAAqB,EAAE,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAC7I,OAAO,EAAmE,KAAK,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAGnH,8BAA8B;AAC9B,MAAM,MAAM,sBAAsB;AAChC,2CAA2C;AACzC,YAAY;AACd,uHAAuH;GACrH,SAAS;AACX,uFAAuF;GACrF,aAAa;AACf,mEAAmE;GACjE,aAAa;AACf,0EAA0E;GACxE,gBAAgB,CAAC;AAErB,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,8HAA8H;IAC9H,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,4GAA4G;IAC5G,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,sBAAsB;IACrC,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,sGAAsG;IACtG,OAAO,EAAE,wBAAwB,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,kBAAkB;IACjC,0CAA0C;IAC1C,KAAK,EAAE,sBAAsB,EAAE,CAAC;IAChC,8IAA8I;IAC9I,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,iGAAiG;IACjG,GAAG,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IACjC,mEAAmE;IACnE,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,mHAAmH;IACnH,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC;AAQD,8GAA8G;AAC9G,wBAAgB,0BAA0B,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,qBAAqB,GAAG,MAAM,EAAE,CAYrH;AAED,6FAA6F;AAC7F,wBAAgB,0BAA0B,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,EAAE,CAE7F;AAED,mJAAmJ;AACnJ,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,EAAE,CAOnE;AAED,oGAAoG;AACpG,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,mGAAmG;IACnG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,oBAAoB,EAAE,CAoB5G;
|
|
1
|
+
{"version":3,"file":"affected.d.ts","sourceRoot":"","sources":["../../src/terragrunt/affected.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Affected Terragrunt units for a git range (#3415): what Terragrunt's own
|
|
3
|
+
* git filter selects, plus three cases it misses, each unit with its reasons.
|
|
4
|
+
*
|
|
5
|
+
* Terragrunt's `[base...head]` filter is the starting point, so the selection
|
|
6
|
+
* matches what `run --all` with that filter would run. Terragrunt 1.1.6 and
|
|
7
|
+
* 1.2.0-rc1 both miss these (observed on `../__fixtures__/terragrunt/affected`):
|
|
8
|
+
*
|
|
9
|
+
* - `module-file`: a file under a unit's local module directory that is not a
|
|
10
|
+
* configuration file, such as one the module reads with `file()`. A unit's
|
|
11
|
+
* module directory is where the `.tf` and `.tofu` files in its `reading`
|
|
12
|
+
* list live, so an interpolated `terraform.source` resolves the way
|
|
13
|
+
* Terragrunt resolved it. A file Terragrunt lists in `reading` is its own
|
|
14
|
+
* to track and is not counted here.
|
|
15
|
+
* - `module-call`: a file under a local module that the unit's module calls,
|
|
16
|
+
* followed through modules that call modules with the same reader
|
|
17
|
+
* `../changed-roots.ts` uses for plain roots.
|
|
18
|
+
* - `stack-template`: a file under a stack's local unit template selects the
|
|
19
|
+
* units generated from it. A stack template (a `stack` block) selects every
|
|
20
|
+
* unit generated under it.
|
|
21
|
+
*
|
|
22
|
+
* Dependents of the selected units are the caller's to add (`terragruntDependents`).
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
25
|
+
import { join, posix, relative, resolve, sep } from "node:path";
|
|
26
|
+
import { rootWatchPaths } from "../changed-roots.js";
|
|
27
|
+
import { excludeFilter, parseTerragruntFind, TERRAGRUNT_DISCOVERY_EXCLUDES } from "./units.js";
|
|
28
|
+
import { defaultTerragruntExec, discoverTerragruntUnits, TerragruntError } from "./run.js";
|
|
29
|
+
import { terragruntEnv } from "./wave.js";
|
|
30
|
+
const KIND_ORDER = ["terragrunt", "removed", "module-file", "module-call", "stack-template"];
|
|
31
|
+
const CONFIG_EXT = /\.(tf|tofu)(\.json)?$/;
|
|
32
|
+
const toPosix = (p) => p.split(sep).join("/");
|
|
33
|
+
const clean = (p) => posix.normalize(toPosix(p)).replace(/^\.\//, "").replace(/\/+$/, "");
|
|
34
|
+
const under = (dir, file) => dir === "." || file === dir || file.startsWith(`${dir}/`);
|
|
35
|
+
/** Arguments to `terragrunt find` for the units Terragrunt's git filter selects between `base` and `head`. */
|
|
36
|
+
export function terragruntAffectedFindArgs(options) {
|
|
37
|
+
const excludes = [...TERRAGRUNT_DISCOVERY_EXCLUDES, ...(options.exclude ?? [])];
|
|
38
|
+
return [
|
|
39
|
+
"find",
|
|
40
|
+
"--json",
|
|
41
|
+
"--no-color",
|
|
42
|
+
// A filters file is unioned with --filter, so it would add every unit it names to the range.
|
|
43
|
+
"--no-filters-file",
|
|
44
|
+
"--filter",
|
|
45
|
+
`[${options.base}...${options.head ?? "HEAD"}]`,
|
|
46
|
+
...excludes.flatMap((g) => ["--filter", excludeFilter(g)]),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
/** Arguments to `git` for the files the range changed, relative to the project directory. */
|
|
50
|
+
export function terragruntAffectedDiffArgs(options) {
|
|
51
|
+
return ["diff", "--name-only", "--no-renames", "--relative", `${options.base}...${options.head ?? "HEAD"}`];
|
|
52
|
+
}
|
|
53
|
+
/** The unit's local module directories: where the `.tf` and `.tofu` files Terragrunt reports it reading live, outside the unit's own directory. */
|
|
54
|
+
export function terragruntModuleDirs(unit) {
|
|
55
|
+
const dirs = new Set();
|
|
56
|
+
for (const f of unit.reading ?? []) {
|
|
57
|
+
if (!CONFIG_EXT.test(f) || under(unit.path, f) || f.includes(".terragrunt-cache/"))
|
|
58
|
+
continue;
|
|
59
|
+
dirs.add(posix.dirname(f));
|
|
60
|
+
}
|
|
61
|
+
return [...dirs].sort();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The `unit` and `stack` blocks of a stack file at `stackFile` (relative to
|
|
65
|
+
* `dir`). Sources that are relative paths or start with `${get_terragrunt_dir()}`
|
|
66
|
+
* are local; anything else (git, registry, other functions) is left without a
|
|
67
|
+
* template, since its version lives in the stack file and a change to it is
|
|
68
|
+
* Terragrunt's to see.
|
|
69
|
+
*/
|
|
70
|
+
export function readTerragruntStackFile(dir, stackFile, text) {
|
|
71
|
+
const stackDir = posix.dirname(clean(stackFile));
|
|
72
|
+
const blocks = [];
|
|
73
|
+
const head = /\b(unit|stack)\s+"([^"]+)"\s*\{/g;
|
|
74
|
+
for (let m = head.exec(text); m; m = head.exec(text)) {
|
|
75
|
+
const body = blockBody(text, head.lastIndex);
|
|
76
|
+
const source = topLevelString(body, "source");
|
|
77
|
+
const path = topLevelString(body, "path");
|
|
78
|
+
if (path === undefined)
|
|
79
|
+
continue;
|
|
80
|
+
const noDot = /^\s*no_dot_terragrunt_stack\s*=\s*true\b/m.test(body);
|
|
81
|
+
const base = noDot ? stackDir : posix.join(stackDir, ".terragrunt-stack");
|
|
82
|
+
const generated = clean(posix.join(base, path));
|
|
83
|
+
let template;
|
|
84
|
+
if (source !== undefined) {
|
|
85
|
+
const local = source.replace(/^\$\{get_terragrunt_dir\(\)\}\//, "./");
|
|
86
|
+
if (/^\.{1,2}\//.test(local))
|
|
87
|
+
template = clean(toPosix(relative(dir, resolve(dir, stackDir, local.replace("//", "/")))));
|
|
88
|
+
}
|
|
89
|
+
blocks.push({ type: m[1], name: m[2], generated, ...(template !== undefined ? { template } : {}) });
|
|
90
|
+
}
|
|
91
|
+
return blocks;
|
|
92
|
+
}
|
|
93
|
+
/** The text between the brace before `start` and its matching close, skipping strings and comments. */
|
|
94
|
+
function blockBody(text, start) {
|
|
95
|
+
let depth = 1;
|
|
96
|
+
for (let i = start; i < text.length; i++) {
|
|
97
|
+
const c = text[i];
|
|
98
|
+
if (c === '"') {
|
|
99
|
+
for (i++; i < text.length && text[i] !== '"'; i++)
|
|
100
|
+
if (text[i] === "\\")
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
else if (c === "#" || (c === "/" && text[i + 1] === "/")) {
|
|
104
|
+
while (i < text.length && text[i] !== "\n")
|
|
105
|
+
i++;
|
|
106
|
+
}
|
|
107
|
+
else if (c === "{")
|
|
108
|
+
depth++;
|
|
109
|
+
else if (c === "}" && --depth === 0)
|
|
110
|
+
return text.slice(start, i);
|
|
111
|
+
}
|
|
112
|
+
return text.slice(start);
|
|
113
|
+
}
|
|
114
|
+
/** A string attribute of a block body at nesting depth zero (`path = "web"`, not one inside `values = { ... }`). */
|
|
115
|
+
function topLevelString(body, name) {
|
|
116
|
+
const attr = new RegExp(`^\\s*${name}\\s*=\\s*"([^"]*)"`);
|
|
117
|
+
let depth = 0;
|
|
118
|
+
for (const line of body.split("\n")) {
|
|
119
|
+
if (depth === 0) {
|
|
120
|
+
const m = attr.exec(line);
|
|
121
|
+
if (m)
|
|
122
|
+
return m[1];
|
|
123
|
+
}
|
|
124
|
+
const code = line.replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
125
|
+
depth += (code.match(/[{[]/g) ?? []).length - (code.match(/[}\]]/g) ?? []).length;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
// `.chant` holds work worktrees in `.chant/work`, each a copy of the checkout (chant#2893).
|
|
130
|
+
const SKIP_DIRS = new Set([".git", ".terragrunt-cache", "node_modules", ".terraform", ".chant"]);
|
|
131
|
+
/** Every `terragrunt.stack.hcl` under `dir`, generated ones included, relative to `dir`. */
|
|
132
|
+
export function findTerragruntStackFiles(dir) {
|
|
133
|
+
const out = [];
|
|
134
|
+
const walk = (rel) => {
|
|
135
|
+
let entries;
|
|
136
|
+
try {
|
|
137
|
+
entries = readdirSync(join(dir, rel), { withFileTypes: true });
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
for (const e of entries) {
|
|
143
|
+
const p = rel ? `${rel}/${e.name}` : e.name;
|
|
144
|
+
if (e.isDirectory() && !SKIP_DIRS.has(e.name))
|
|
145
|
+
walk(p);
|
|
146
|
+
else if (e.isFile() && e.name === "terragrunt.stack.hcl")
|
|
147
|
+
out.push(p);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
walk("");
|
|
151
|
+
return out.sort();
|
|
152
|
+
}
|
|
153
|
+
/** The stack file a generated unit came from: the one beside the `.terragrunt-stack` it sits in. */
|
|
154
|
+
function generatingStackFile(unitPath) {
|
|
155
|
+
const i = unitPath.lastIndexOf("/.terragrunt-stack/");
|
|
156
|
+
return i < 0 ? undefined : `${unitPath.slice(0, i)}/terragrunt.stack.hcl`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The affected units: what Terragrunt selected, each with the changed files
|
|
160
|
+
* that explain it, plus the three supplements. A unit Terragrunt selected
|
|
161
|
+
* that discovery does not list is `removed` when its `terragrunt.hcl` is gone,
|
|
162
|
+
* and otherwise left out with a note (discovery excluded it).
|
|
163
|
+
*/
|
|
164
|
+
export function terragruntAffected(input) {
|
|
165
|
+
const changed = [...new Set(input.changedFiles.map(clean))].sort();
|
|
166
|
+
const byPath = new Map(input.units.map((u) => [u.path, u]));
|
|
167
|
+
const reasons = new Map();
|
|
168
|
+
const notes = [];
|
|
169
|
+
const add = (path, reason) => {
|
|
170
|
+
const list = reasons.get(path) ?? [];
|
|
171
|
+
list.push(reason);
|
|
172
|
+
reasons.set(path, list);
|
|
173
|
+
};
|
|
174
|
+
for (const raw of new Set(input.terragruntSelected.map(clean))) {
|
|
175
|
+
const unit = byPath.get(raw);
|
|
176
|
+
if (!unit) {
|
|
177
|
+
if (existsSync(join(input.dir, raw, "terragrunt.hcl"))) {
|
|
178
|
+
notes.push(`${raw}: Terragrunt's git range selected it, but discovery left it out, so it is not affected here`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
add(raw, { kind: "removed", files: changed.filter((f) => under(raw, f)) });
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const tracked = new Set([...(unit.reading ?? []), ...Object.values(unit.include ?? {}), generatingStackFile(unit.path)].filter((f) => !!f));
|
|
186
|
+
add(raw, { kind: "terragrunt", files: changed.filter((f) => under(unit.path, f) || tracked.has(f)) });
|
|
187
|
+
}
|
|
188
|
+
for (const unit of input.units) {
|
|
189
|
+
const reading = new Set(unit.reading ?? []);
|
|
190
|
+
const moduleDirs = terragruntModuleDirs(unit);
|
|
191
|
+
const called = new Map();
|
|
192
|
+
for (const m of moduleDirs) {
|
|
193
|
+
for (const d of rootWatchPaths(input.dir, { dir: m }))
|
|
194
|
+
if (!moduleDirs.includes(d) && !called.has(d))
|
|
195
|
+
called.set(d, m);
|
|
196
|
+
}
|
|
197
|
+
const viaFile = new Map();
|
|
198
|
+
const viaCall = new Map();
|
|
199
|
+
for (const f of changed) {
|
|
200
|
+
if (reading.has(f))
|
|
201
|
+
continue;
|
|
202
|
+
const call = [...called.keys()].filter((d) => under(d, f)).sort((a, b) => b.length - a.length)[0];
|
|
203
|
+
if (call !== undefined) {
|
|
204
|
+
viaCall.set(call, [...(viaCall.get(call) ?? []), f]);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const own = moduleDirs.filter((d) => under(d, f)).sort((a, b) => b.length - a.length)[0];
|
|
208
|
+
if (own !== undefined)
|
|
209
|
+
viaFile.set(own, [...(viaFile.get(own) ?? []), f]);
|
|
210
|
+
}
|
|
211
|
+
for (const [via, files] of viaFile)
|
|
212
|
+
add(unit.path, { kind: "module-file", files, via });
|
|
213
|
+
for (const [via, files] of viaCall)
|
|
214
|
+
add(unit.path, { kind: "module-call", files, via });
|
|
215
|
+
}
|
|
216
|
+
for (const stackFile of findTerragruntStackFiles(input.dir)) {
|
|
217
|
+
let text;
|
|
218
|
+
try {
|
|
219
|
+
text = readFileSync(join(input.dir, stackFile), "utf8");
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
for (const block of readTerragruntStackFile(input.dir, stackFile, text)) {
|
|
225
|
+
if (block.template === undefined)
|
|
226
|
+
continue;
|
|
227
|
+
const files = changed.filter((f) => under(block.template, f));
|
|
228
|
+
if (files.length === 0)
|
|
229
|
+
continue;
|
|
230
|
+
const targets = block.type === "unit" ? (byPath.has(block.generated) ? [block.generated] : []) : input.units.filter((u) => under(block.generated, u.path)).map((u) => u.path);
|
|
231
|
+
if (targets.length === 0) {
|
|
232
|
+
notes.push(`${block.generated}: generated by ${block.type} "${block.name}" in ${stackFile} from the changed template ${block.template}, ` +
|
|
233
|
+
"but discovery did not list it; run `terragrunt stack generate` before discovery");
|
|
234
|
+
}
|
|
235
|
+
for (const t of targets)
|
|
236
|
+
add(t, { kind: "stack-template", files, via: block.template });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const units = [...reasons.entries()]
|
|
240
|
+
.map(([path, list]) => ({
|
|
241
|
+
path,
|
|
242
|
+
reasons: list.sort((a, b) => KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind) || (a.via ?? "").localeCompare(b.via ?? "")),
|
|
243
|
+
}))
|
|
244
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
245
|
+
return { units, notes: [...new Set(notes)].sort() };
|
|
246
|
+
}
|
|
247
|
+
/** One line per reason, for a report or a job log. */
|
|
248
|
+
export function describeTerragruntAffectedReason(reason) {
|
|
249
|
+
const files = reason.files.join(", ");
|
|
250
|
+
switch (reason.kind) {
|
|
251
|
+
case "terragrunt":
|
|
252
|
+
return files ? `Terragrunt's git range selected it (${files})` : "Terragrunt's git range selected it";
|
|
253
|
+
case "removed":
|
|
254
|
+
return "removed in the range; Terragrunt plans its destroy";
|
|
255
|
+
case "module-file":
|
|
256
|
+
return `${files} changed under its module ${reason.via}, which Terragrunt does not track`;
|
|
257
|
+
case "module-call":
|
|
258
|
+
return `${files} changed under ${reason.via}, a module its module calls`;
|
|
259
|
+
case "stack-template":
|
|
260
|
+
return `${files} changed under ${reason.via}, the stack template it is generated from`;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/** Run git and Terragrunt's git filter in `dir` and return the affected units with their reasons. */
|
|
264
|
+
export async function findTerragruntAffected(options) {
|
|
265
|
+
const exec = options.exec ?? defaultTerragruntExec;
|
|
266
|
+
const tg = options.terragrunt ?? "terragrunt";
|
|
267
|
+
const git = options.git ?? "git";
|
|
268
|
+
const diffArgs = terragruntAffectedDiffArgs(options);
|
|
269
|
+
const diff = await exec(git, diffArgs, { cwd: options.dir, env: {} });
|
|
270
|
+
if (diff.code !== 0)
|
|
271
|
+
throw new TerragruntError(`\`${git} ${diffArgs.join(" ")}\` failed (exit ${diff.code}): ${diff.stderr.trim()}`);
|
|
272
|
+
const findArgs = terragruntAffectedFindArgs(options);
|
|
273
|
+
const found = await exec(tg, findArgs, { cwd: options.dir, env: terragruntEnv(options.binary) });
|
|
274
|
+
if (found.code !== 0)
|
|
275
|
+
throw new TerragruntError(`\`${tg} ${findArgs.join(" ")}\` failed (exit ${found.code}): ${found.stderr.trim()}`);
|
|
276
|
+
let selected;
|
|
277
|
+
try {
|
|
278
|
+
selected = parseTerragruntFind(found.stdout).map((u) => u.path);
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
throw new TerragruntError(`could not read \`terragrunt find\` output: ${err instanceof Error ? err.message : String(err)}`);
|
|
282
|
+
}
|
|
283
|
+
const units = options.units ?? (await discoverTerragruntUnits(options)).units;
|
|
284
|
+
return terragruntAffected({
|
|
285
|
+
dir: options.dir,
|
|
286
|
+
units,
|
|
287
|
+
terragruntSelected: selected,
|
|
288
|
+
changedFiles: diff.stdout.split("\n").map((l) => l.trim()).filter((l) => l !== ""),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terragrunt units as roots (#3414). `./units`, `./wave` and `./mocks` are pure and
|
|
3
|
+
* bundle without TypeScript; `./run` spawns `terragrunt`. `./affected` picks
|
|
4
|
+
* the units a git range touches (#3415).
|
|
5
|
+
*/
|
|
6
|
+
export * from "./units.js";
|
|
7
|
+
export * from "./wave.js";
|
|
8
|
+
export * from "./mocks.js";
|
|
9
|
+
export * from "./run.js";
|
|
10
|
+
export * from "./affected.js";
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock outputs in Terragrunt waves (#3416): which plans of a wave would read
|
|
3
|
+
* a dependency's `mock_outputs`, and the provisional mark a preview carries.
|
|
4
|
+
*
|
|
5
|
+
* A plan built on `mock_outputs` looks like a real one: Terragrunt says so in
|
|
6
|
+
* one log line only, and the plan JSON holds the mock values as if they were
|
|
7
|
+
* the upstream's (observed on 1.1.6). Gruntwork closed mocks in saved plans
|
|
8
|
+
* as won't-fix (gruntwork-io/terragrunt#2178). So a wave is checked before it
|
|
9
|
+
* is planned, and a wave that would read a mock is refused: it is not gated,
|
|
10
|
+
* and the refusal names the upstream that must apply first.
|
|
11
|
+
*
|
|
12
|
+
* The rules follow Terragrunt's own (`pkg/config/dependency.go` at v1.1.6,
|
|
13
|
+
* `getTerragruntOutputIfAppliedElseConfiguredDefault` and
|
|
14
|
+
* `shouldReturnMockOutputs`). For one `dependency` block, read at `plan`:
|
|
15
|
+
*
|
|
16
|
+
* - `enabled = false` or `skip_outputs = true`: Terragrunt never reads the
|
|
17
|
+
* upstream, so with `mock_outputs` set the unit always reads the mock.
|
|
18
|
+
* - The upstream has no outputs (`output -json` prints `{}`, as it does for
|
|
19
|
+
* a unit never applied): Terragrunt returns `mock_outputs` when they are
|
|
20
|
+
* set and allowed for `plan`, and fails otherwise. Either way the wave
|
|
21
|
+
* cannot plan on real values yet.
|
|
22
|
+
* - The upstream has outputs and the block sets a merge strategy
|
|
23
|
+
* (`mock_outputs_merge_strategy_with_state`, or the older
|
|
24
|
+
* `mock_outputs_merge_with_state = true`): every mock key the real outputs
|
|
25
|
+
* lack is filled from the mock. That is a partial mock, and it counts.
|
|
26
|
+
*
|
|
27
|
+
* The blocks come from `terragrunt render --json`, so includes, locals and
|
|
28
|
+
* expressions in `config_path` are Terragrunt's to evaluate, not chant's.
|
|
29
|
+
*
|
|
30
|
+
* A provisional plan is the other side: a dependent planned at PR time
|
|
31
|
+
* (`dependents: plan`), before its upstream applied. It may read mocks. Its
|
|
32
|
+
* change-set members carry `provisional: true`, which keeps it out of the
|
|
33
|
+
* change-set digest, out of any wave's set digest and out of every group of
|
|
34
|
+
* real plans, and its saved plans are never applied.
|
|
35
|
+
*
|
|
36
|
+
* Pure: nothing here spawns or reads files. `./run.ts` runs the commands.
|
|
37
|
+
*/
|
|
38
|
+
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
39
|
+
/** `a/b/../c` to `a/c`, with `/` separators and no `./` or trailing `/`. An absolute path keeps its leading `/`. */
|
|
40
|
+
function normalize(path) {
|
|
41
|
+
const p = path.replace(/\\/g, "/");
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const seg of p.split("/")) {
|
|
44
|
+
if (seg === "" || seg === ".")
|
|
45
|
+
continue;
|
|
46
|
+
if (seg === ".." && out.length > 0 && out[out.length - 1] !== "..")
|
|
47
|
+
out.pop();
|
|
48
|
+
else
|
|
49
|
+
out.push(seg);
|
|
50
|
+
}
|
|
51
|
+
const s = out.join("/");
|
|
52
|
+
return p.startsWith("/") ? `/${s}` : s || ".";
|
|
53
|
+
}
|
|
54
|
+
const isAbsolute = (p) => p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
|
|
55
|
+
/**
|
|
56
|
+
* The upstream a `config_path` names, relative to where Terragrunt runs. A
|
|
57
|
+
* relative path is taken from the unit's directory. An absolute one is made
|
|
58
|
+
* relative to the first of `dirs` it sits under (a directory and its real
|
|
59
|
+
* path, when a symlink leads to it), and kept absolute when it sits under
|
|
60
|
+
* none. A path to the upstream's `terragrunt.hcl` names its directory.
|
|
61
|
+
*/
|
|
62
|
+
export function upstreamOfConfigPath(unit, configPath, dirs) {
|
|
63
|
+
const p = configPath.replace(/\\/g, "/").replace(/(^|\/)terragrunt\.hcl$/, "");
|
|
64
|
+
if (!isAbsolute(p))
|
|
65
|
+
return normalize(`${unit}/${p}`);
|
|
66
|
+
const abs = normalize(p);
|
|
67
|
+
for (const dir of typeof dirs === "string" ? [dirs] : dirs) {
|
|
68
|
+
const root = normalize(dir);
|
|
69
|
+
if (abs === root)
|
|
70
|
+
return ".";
|
|
71
|
+
if (abs.startsWith(`${root}/`))
|
|
72
|
+
return abs.slice(root.length + 1);
|
|
73
|
+
}
|
|
74
|
+
return abs;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The `dependency` blocks of one unit, from `terragrunt render --json` run
|
|
78
|
+
* in it. `unit` is the unit's path relative to where Terragrunt runs (`dirs`).
|
|
79
|
+
* Throws when the output is not the rendered config.
|
|
80
|
+
*/
|
|
81
|
+
export function parseRenderedDependencies(unit, output, dirs) {
|
|
82
|
+
const doc = typeof output === "string" ? JSON.parse(output) : output;
|
|
83
|
+
if (!isObject(doc))
|
|
84
|
+
throw new Error("terragrunt render --json did not print an object");
|
|
85
|
+
const blocks = isObject(doc.dependency) ? doc.dependency : {};
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const [label, raw] of Object.entries(blocks)) {
|
|
88
|
+
if (!isObject(raw) || typeof raw.config_path !== "string")
|
|
89
|
+
continue;
|
|
90
|
+
const allowed = Array.isArray(raw.mock_outputs_allowed_terraform_commands)
|
|
91
|
+
? raw.mock_outputs_allowed_terraform_commands.filter((c) => typeof c === "string")
|
|
92
|
+
: undefined;
|
|
93
|
+
const strategy = typeof raw.mock_outputs_merge_strategy_with_state === "string"
|
|
94
|
+
? raw.mock_outputs_merge_strategy_with_state
|
|
95
|
+
: raw.mock_outputs_merge_with_state === true
|
|
96
|
+
? "shallow"
|
|
97
|
+
: undefined;
|
|
98
|
+
out.push({
|
|
99
|
+
name: typeof raw.name === "string" && raw.name ? raw.name : label,
|
|
100
|
+
upstream: upstreamOfConfigPath(unit, raw.config_path, dirs),
|
|
101
|
+
...(isObject(raw.mock_outputs) ? { mockOutputs: raw.mock_outputs } : {}),
|
|
102
|
+
...(allowed ? { mockAllowed: allowed } : {}),
|
|
103
|
+
...(strategy && strategy !== "no_merge" ? { mergeStrategy: strategy } : {}),
|
|
104
|
+
// render prints skip_outputs under `skip`.
|
|
105
|
+
...(raw.skip === true || raw.skip_outputs === true ? { skipOutputs: true } : {}),
|
|
106
|
+
...(raw.enabled === false ? { disabled: true } : {}),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return out.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The outputs `output -json` prints, by name, with their values. Undefined
|
|
113
|
+
* when the text holds no JSON object.
|
|
114
|
+
*/
|
|
115
|
+
export function parseTerragruntOutputs(output) {
|
|
116
|
+
const text = output.trim();
|
|
117
|
+
const start = text.indexOf("{");
|
|
118
|
+
if (start < 0)
|
|
119
|
+
return undefined;
|
|
120
|
+
let doc;
|
|
121
|
+
try {
|
|
122
|
+
doc = JSON.parse(text.slice(start));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
if (!isObject(doc))
|
|
128
|
+
return undefined;
|
|
129
|
+
return Object.fromEntries(Object.entries(doc).map(([k, v]) => [k, isObject(v) && "value" in v ? v.value : v]));
|
|
130
|
+
}
|
|
131
|
+
/** Arguments to `terragrunt` that print one unit's outputs as JSON. */
|
|
132
|
+
export function terragruntOutputArgs(unit) {
|
|
133
|
+
return ["run", "--working-dir", unit, "--non-interactive", "--no-color", "--", "output", "-json"];
|
|
134
|
+
}
|
|
135
|
+
/** Arguments to `terragrunt` that print one unit's evaluated configuration as JSON. */
|
|
136
|
+
export function terragruntRenderArgs(unit) {
|
|
137
|
+
return ["render", "--json", "--non-interactive", "--no-color", "--working-dir", unit];
|
|
138
|
+
}
|
|
139
|
+
/** Mock paths the real outputs lack. `shallow` compares top-level keys; the deep strategies follow nested maps. */
|
|
140
|
+
function missingKeys(mock, real, deep, prefix = "") {
|
|
141
|
+
const out = [];
|
|
142
|
+
for (const [k, v] of Object.entries(mock)) {
|
|
143
|
+
const path = prefix ? `${prefix}.${k}` : k;
|
|
144
|
+
if (!(k in real))
|
|
145
|
+
out.push(path);
|
|
146
|
+
else if (deep && isObject(v) && isObject(real[k]))
|
|
147
|
+
out.push(...missingKeys(v, real[k], deep, path));
|
|
148
|
+
}
|
|
149
|
+
return out.sort();
|
|
150
|
+
}
|
|
151
|
+
const allowsPlan = (d) => !d.mockAllowed || d.mockAllowed.length === 0 || d.mockAllowed.includes("plan");
|
|
152
|
+
/**
|
|
153
|
+
* Every dependency of the wave's units that would read `mock_outputs` at
|
|
154
|
+
* plan, sorted by unit and dependency. Empty when the wave plans on real
|
|
155
|
+
* outputs only.
|
|
156
|
+
*
|
|
157
|
+
* An upstream with no outputs is listed whether or not the block sets
|
|
158
|
+
* mocks: without them Terragrunt fails the plan, and with them it plans on
|
|
159
|
+
* fake values. Both mean the upstream must apply first.
|
|
160
|
+
*/
|
|
161
|
+
export function terragruntMockReads(input) {
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const [unit, deps] of input.dependencies) {
|
|
164
|
+
for (const d of deps) {
|
|
165
|
+
const at = { unit, dependency: d.name, upstream: d.upstream };
|
|
166
|
+
if (d.disabled || d.skipOutputs) {
|
|
167
|
+
if (d.mockOutputs)
|
|
168
|
+
out.push({ ...at, reason: d.disabled ? "disabled" : "skip-outputs" });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const real = input.outputs.get(d.upstream) ?? {};
|
|
172
|
+
if (Object.keys(real).length === 0) {
|
|
173
|
+
out.push({ ...at, reason: "no-outputs" });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (d.mockOutputs && d.mergeStrategy && allowsPlan(d)) {
|
|
177
|
+
const keys = missingKeys(d.mockOutputs, real, d.mergeStrategy !== "shallow");
|
|
178
|
+
if (keys.length > 0)
|
|
179
|
+
out.push({ ...at, reason: "partial", keys });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return out.sort((a, b) => (a.unit < b.unit ? -1 : a.unit > b.unit ? 1 : a.dependency < b.dependency ? -1 : a.dependency > b.dependency ? 1 : 0));
|
|
184
|
+
}
|
|
185
|
+
/** The upstreams a wave's `dependency` blocks read outputs from, sorted. */
|
|
186
|
+
export function terragruntOutputUpstreams(dependencies) {
|
|
187
|
+
const out = new Set();
|
|
188
|
+
for (const deps of dependencies.values())
|
|
189
|
+
for (const d of deps)
|
|
190
|
+
if (!d.disabled && !d.skipOutputs)
|
|
191
|
+
out.add(d.upstream);
|
|
192
|
+
return [...out].sort();
|
|
193
|
+
}
|
|
194
|
+
function readText(r) {
|
|
195
|
+
const at = `${r.unit} (dependency "${r.dependency}" on ${r.upstream})`;
|
|
196
|
+
switch (r.reason) {
|
|
197
|
+
case "no-outputs":
|
|
198
|
+
return `${at}: ${r.upstream} has no outputs yet; apply ${r.upstream} first`;
|
|
199
|
+
case "partial":
|
|
200
|
+
return `${at}: ${r.upstream}'s outputs lack ${r.keys.join(", ")}, which the merge strategy fills from mock_outputs; apply ${r.upstream} with those outputs first, or drop the merge strategy`;
|
|
201
|
+
case "skip-outputs":
|
|
202
|
+
return `${at}: skip_outputs = true with mock_outputs set always reads the mock; read the real outputs or drop mock_outputs`;
|
|
203
|
+
case "disabled":
|
|
204
|
+
return `${at}: enabled = false with mock_outputs set always reads the mock; enable the dependency or drop mock_outputs`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** The refusal for a wave whose plans would read mocks: each unit, the upstream, and what must happen first. */
|
|
208
|
+
export function describeMockRefusal(reads) {
|
|
209
|
+
const upstreams = [...new Set(reads.filter((r) => r.reason === "no-outputs" || r.reason === "partial").map((r) => r.upstream))].sort();
|
|
210
|
+
const head = `this wave was not planned and is not gated: its plans would read mock_outputs, so nobody could approve them as real. ` +
|
|
211
|
+
(upstreams.length > 0 ? `Apply ${upstreams.join(", ")} first, then plan the wave again.` : `Fix the dependency blocks below, then plan the wave again.`);
|
|
212
|
+
return [head, ...reads.map((r) => ` ${readText(r)}`)].join("\n");
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The line Terragrunt logs when it hands a unit `mock_outputs` because the
|
|
216
|
+
* upstream has no outputs (`dependency.go` at v1.1.6): "Config <upstream> is
|
|
217
|
+
* a dependency of <unit> that has no outputs, but mock outputs provided and
|
|
218
|
+
* returning those in dependency output."
|
|
219
|
+
*/
|
|
220
|
+
const MOCK_WARNING = /Config (\S+?) is a dependency of (\S+?) that has no outputs, but mock outputs provided/g;
|
|
221
|
+
/**
|
|
222
|
+
* The units a plan log says read mocks, with the upstream each read,
|
|
223
|
+
* relative to where Terragrunt ran (`dirs`, as for
|
|
224
|
+
* {@link upstreamOfConfigPath}). This is the check after the fact: the check
|
|
225
|
+
* before planning ({@link terragruntMockReads}) is what refuses a wave, and
|
|
226
|
+
* this catches an upstream whose state went away in between.
|
|
227
|
+
*/
|
|
228
|
+
export function terragruntMockWarnings(log, dirs) {
|
|
229
|
+
const seen = new Set();
|
|
230
|
+
const out = [];
|
|
231
|
+
for (const m of log.matchAll(MOCK_WARNING)) {
|
|
232
|
+
const unit = upstreamOfConfigPath(".", m[2], dirs);
|
|
233
|
+
const upstream = upstreamOfConfigPath(".", m[1], dirs);
|
|
234
|
+
const key = `${unit}\u0000${upstream}`;
|
|
235
|
+
if (seen.has(key))
|
|
236
|
+
continue;
|
|
237
|
+
seen.add(key);
|
|
238
|
+
out.push({ unit, upstream });
|
|
239
|
+
}
|
|
240
|
+
return out.sort((a, b) => (a.unit < b.unit ? -1 : a.unit > b.unit ? 1 : a.upstream < b.upstream ? -1 : 1));
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* The parts with each member whose unit the plan log says read mocks turned
|
|
244
|
+
* into a failed member, so a mock plan never reaches a gate.
|
|
245
|
+
*/
|
|
246
|
+
export function failMockedParts(parts, mocked) {
|
|
247
|
+
const by = new Map();
|
|
248
|
+
for (const m of mocked)
|
|
249
|
+
by.set(m.unit, [...(by.get(m.unit) ?? []), m.upstream]);
|
|
250
|
+
return parts.map((p) => {
|
|
251
|
+
const ups = by.get(p.member.member);
|
|
252
|
+
if (!ups)
|
|
253
|
+
return p;
|
|
254
|
+
return {
|
|
255
|
+
member: {
|
|
256
|
+
...p.member,
|
|
257
|
+
status: "failed",
|
|
258
|
+
error: `planned on mock_outputs: ${ups.join(", ")} had no outputs when Terragrunt planned it; apply ${ups.join(", ")} first`,
|
|
259
|
+
planDigest: null,
|
|
260
|
+
},
|
|
261
|
+
entries: [],
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/** The parts of a provisional plan: each member marked, so no digest or group of real plans takes it. */
|
|
266
|
+
export function markProvisional(parts) {
|
|
267
|
+
return parts.map((p) => ({ ...p, member: { ...p.member, provisional: true } }));
|
|
268
|
+
}
|
|
269
|
+
/** The file a provisional plan leaves in its work directory, so its saved plans are never applied. */
|
|
270
|
+
export const PROVISIONAL_MARKER = "PROVISIONAL";
|