@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,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Change-set adapters for the terraform family (#3181): a `terraform show
|
|
3
|
+
* -json` plan from terraform, tofu or choudoufu, and a choudoufu set plan
|
|
4
|
+
* document (`live-plan-set -json`, choudoufu#1752), each into
|
|
5
|
+
* `@intentius/chant/change-set` parts.
|
|
6
|
+
*
|
|
7
|
+
* A member's plan digest is {@link terraformPlanDigest}, the value a
|
|
8
|
+
* `TerraformApplyOp` gate on that root binds (#2300), so a change set over
|
|
9
|
+
* one root and a gate over the same root agree on what changed.
|
|
10
|
+
*
|
|
11
|
+
* This module imports `./plan-digest` and chant's `change-set` and
|
|
12
|
+
* `lifecycle/plan-digest` subpaths, never the lexicon's entry point, so it
|
|
13
|
+
* bundles without TypeScript or the lint rules (#3421).
|
|
14
|
+
*/
|
|
15
|
+
import { terraformPlanDigest } from "./plan-digest.js";
|
|
16
|
+
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
17
|
+
/** Whether a sensitivity or unknown map marks anything under it `true`. */
|
|
18
|
+
function marks(v) {
|
|
19
|
+
if (v === true)
|
|
20
|
+
return true;
|
|
21
|
+
if (Array.isArray(v))
|
|
22
|
+
return v.some(marks);
|
|
23
|
+
if (isObject(v))
|
|
24
|
+
return Object.values(v).some(marks);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
function sameJson(a, b) {
|
|
28
|
+
if (a === b)
|
|
29
|
+
return true;
|
|
30
|
+
if (Array.isArray(a) && Array.isArray(b))
|
|
31
|
+
return a.length === b.length && a.every((x, i) => sameJson(x, b[i]));
|
|
32
|
+
if (isObject(a) && isObject(b)) {
|
|
33
|
+
const ka = Object.keys(a);
|
|
34
|
+
return ka.length === Object.keys(b).length && ka.every((k) => k in b && sameJson(a[k], b[k]));
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The planner a root's binary names: `choudoufu` and `tofu` by name, and
|
|
40
|
+
* `terraform` for anything else, a path to terraform included.
|
|
41
|
+
*/
|
|
42
|
+
export function plannerForBinary(binary) {
|
|
43
|
+
const base = (binary ?? "terraform").split(/[\\/]/).pop() ?? "";
|
|
44
|
+
if (base.startsWith("choudoufu"))
|
|
45
|
+
return "choudoufu";
|
|
46
|
+
if (base.startsWith("tofu"))
|
|
47
|
+
return "tofu";
|
|
48
|
+
return "terraform";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The action a `resource_changes` entry's `actions` array means. Delete then
|
|
52
|
+
* create and create then delete are both a replace; the order is the
|
|
53
|
+
* disruption ({@link disruptionFor}).
|
|
54
|
+
*/
|
|
55
|
+
export function actionFor(actions) {
|
|
56
|
+
const a = Array.isArray(actions) ? actions.map(String) : [];
|
|
57
|
+
if (a.length === 2 && a.includes("delete") && a.includes("create"))
|
|
58
|
+
return "replace";
|
|
59
|
+
if (a.length === 1) {
|
|
60
|
+
switch (a[0]) {
|
|
61
|
+
case "create":
|
|
62
|
+
case "update":
|
|
63
|
+
case "delete":
|
|
64
|
+
case "read":
|
|
65
|
+
case "no-op":
|
|
66
|
+
case "forget":
|
|
67
|
+
return a[0];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`a plan entry has actions ${JSON.stringify(actions)}, which the change set has no action for`);
|
|
71
|
+
}
|
|
72
|
+
function disruptionFor(actions, action) {
|
|
73
|
+
switch (action) {
|
|
74
|
+
case "update": return "in-place";
|
|
75
|
+
case "delete": return "destroy";
|
|
76
|
+
case "replace": return actions[0] === "create" ? "replace" : "destroy";
|
|
77
|
+
default: return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** A resource address's module path, name and index, read off the address when the entry does not carry them. */
|
|
81
|
+
export function parseTerraformAddress(address) {
|
|
82
|
+
// Module segments, then [data.]type.name[index]. Index brackets may hold a quoted key with dots in it.
|
|
83
|
+
const re = /^((?:module\.[^.[\]]+(?:\[(?:"(?:[^"\\]|\\.)*"|\d+)\])?\.)*)(?:data\.)?([^.[\]]+)\.([^.[\]]+)(?:\[("(?:[^"\\]|\\.)*"|\d+)\])?$/;
|
|
84
|
+
const m = re.exec(address);
|
|
85
|
+
if (!m)
|
|
86
|
+
return { type: "unknown", name: address };
|
|
87
|
+
const module = m[1] ? m[1].slice(0, -1) : undefined;
|
|
88
|
+
const index = m[4] === undefined ? undefined : m[4].startsWith('"') ? JSON.parse(m[4]) : Number(m[4]);
|
|
89
|
+
return { ...(module ? { module } : {}), type: m[2], name: m[3], ...(index !== undefined ? { index } : {}) };
|
|
90
|
+
}
|
|
91
|
+
/** The attributes one change writes, top-level, with sensitive values left out. */
|
|
92
|
+
function attributesFor(change, action) {
|
|
93
|
+
if (action === "delete" || action === "no-op" || action === "forget" || action === "read")
|
|
94
|
+
return [];
|
|
95
|
+
const before = isObject(change.before) ? change.before : {};
|
|
96
|
+
const after = isObject(change.after) ? change.after : {};
|
|
97
|
+
const afterUnknown = isObject(change.after_unknown) ? change.after_unknown : {};
|
|
98
|
+
const beforeSensitive = isObject(change.before_sensitive) ? change.before_sensitive : {};
|
|
99
|
+
const afterSensitive = isObject(change.after_sensitive) ? change.after_sensitive : {};
|
|
100
|
+
const forcing = new Set((Array.isArray(change.replace_paths) ? change.replace_paths : [])
|
|
101
|
+
.map((p) => (Array.isArray(p) ? p[0] : undefined))
|
|
102
|
+
.filter((k) => typeof k === "string"));
|
|
103
|
+
const keys = [...new Set([...Object.keys(before), ...Object.keys(after), ...Object.keys(afterUnknown)])].sort();
|
|
104
|
+
const out = [];
|
|
105
|
+
for (const k of keys) {
|
|
106
|
+
const unknown = marks(afterUnknown[k]);
|
|
107
|
+
const hasBefore = action !== "create" && before[k] !== undefined && before[k] !== null;
|
|
108
|
+
const hasAfter = after[k] !== undefined && after[k] !== null;
|
|
109
|
+
if (action === "create" ? !hasAfter && !unknown : !unknown && sameJson(before[k] ?? null, after[k] ?? null))
|
|
110
|
+
continue;
|
|
111
|
+
const sensitive = marks(beforeSensitive[k]) || marks(afterSensitive[k]);
|
|
112
|
+
out.push({
|
|
113
|
+
path: k,
|
|
114
|
+
...(sensitive ? { sensitive: true } : {
|
|
115
|
+
...(hasBefore ? { before: before[k] } : {}),
|
|
116
|
+
...(hasAfter && !unknown ? { after: after[k] } : {}),
|
|
117
|
+
}),
|
|
118
|
+
...(unknown ? { unknown: true } : {}),
|
|
119
|
+
...(forcing.has(k) ? { forcesReplacement: true } : {}),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
function regionOf(change) {
|
|
125
|
+
for (const side of [change.after, change.before]) {
|
|
126
|
+
if (isObject(side) && typeof side.region === "string" && side.region !== "")
|
|
127
|
+
return side.region;
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* One terraform-family root's part, from its `show -json` plan.
|
|
133
|
+
*
|
|
134
|
+
* Every `resource_changes` entry becomes an entry, no-op and read included.
|
|
135
|
+
* A plan stock marked `errored` is a failed member: its entries are kept,
|
|
136
|
+
* but nothing should apply it.
|
|
137
|
+
*/
|
|
138
|
+
export function terraformChangeSetPart(input) {
|
|
139
|
+
const plan = isObject(input.plan) ? input.plan : {};
|
|
140
|
+
const planner = input.planner ?? "terraform";
|
|
141
|
+
const raw = Array.isArray(plan.resource_changes) ? plan.resource_changes : [];
|
|
142
|
+
const entries = raw.map((r) => {
|
|
143
|
+
const rc = isObject(r) ? r : {};
|
|
144
|
+
const change = isObject(rc.change) ? rc.change : {};
|
|
145
|
+
const address = String(rc.address ?? "");
|
|
146
|
+
const parsed = parseTerraformAddress(address);
|
|
147
|
+
const action = actionFor(change.actions);
|
|
148
|
+
const disruption = disruptionFor(change.actions, action);
|
|
149
|
+
const region = regionOf(change);
|
|
150
|
+
const index = rc.index !== undefined && rc.index !== null ? rc.index : parsed.index;
|
|
151
|
+
const module = typeof rc.module_address === "string" ? rc.module_address : parsed.module;
|
|
152
|
+
return {
|
|
153
|
+
member: input.member,
|
|
154
|
+
lexicon: "terraform",
|
|
155
|
+
planner,
|
|
156
|
+
address,
|
|
157
|
+
type: typeof rc.type === "string" ? rc.type : parsed.type,
|
|
158
|
+
name: typeof rc.name === "string" ? rc.name : parsed.name,
|
|
159
|
+
...(index !== undefined ? { index } : {}),
|
|
160
|
+
...(module ? { module } : {}),
|
|
161
|
+
...(typeof rc.deposed === "string" ? { deposed: rc.deposed } : {}),
|
|
162
|
+
action,
|
|
163
|
+
...(isObject(change.importing) ? { importing: true } : {}),
|
|
164
|
+
...(disruption ? { disruption } : {}),
|
|
165
|
+
...(region ? { region } : {}),
|
|
166
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
167
|
+
attributes: attributesFor(change, action),
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
const sideEffects = (Array.isArray(plan.action_invocations) ? plan.action_invocations : []).map(sideEffectFor);
|
|
171
|
+
const errored = plan.errored === true;
|
|
172
|
+
return {
|
|
173
|
+
member: {
|
|
174
|
+
member: input.member,
|
|
175
|
+
lexicon: "terraform",
|
|
176
|
+
planner,
|
|
177
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
178
|
+
status: errored ? "failed" : "planned",
|
|
179
|
+
...(errored ? { error: `${planner} marked the plan errored` } : {}),
|
|
180
|
+
planDigest: terraformPlanDigest(plan),
|
|
181
|
+
...(input.nativeDigest ? { nativeDigest: input.nativeDigest } : {}),
|
|
182
|
+
holes: [],
|
|
183
|
+
},
|
|
184
|
+
entries,
|
|
185
|
+
...(sideEffects.length > 0 ? { sideEffects } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* One `action_invocations` entry of a plan (Terraform 1.14): the action's
|
|
190
|
+
* address and type, and, for a lifecycle trigger, the resource and event.
|
|
191
|
+
*/
|
|
192
|
+
function sideEffectFor(raw) {
|
|
193
|
+
const inv = isObject(raw) ? raw : {};
|
|
194
|
+
const type = typeof inv.type === "string" ? inv.type : "unknown";
|
|
195
|
+
const lifecycle = isObject(inv.lifecycle_action_trigger) ? inv.lifecycle_action_trigger : {};
|
|
196
|
+
return {
|
|
197
|
+
address: typeof inv.address === "string" ? inv.address : type,
|
|
198
|
+
type,
|
|
199
|
+
...(typeof lifecycle.triggering_resource_address === "string" ? { trigger: lifecycle.triggering_resource_address } : {}),
|
|
200
|
+
...(typeof lifecycle.action_trigger_event === "string" ? { event: lifecycle.action_trigger_event } : {}),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* One part per root of a choudoufu set plan (choudoufu#1752). A planned
|
|
205
|
+
* root is {@link terraformChangeSetPart} over its embedded plan, with the
|
|
206
|
+
* estate as scope and choudoufu's own root digest as `nativeDigest`. A
|
|
207
|
+
* failed root is a failed member with its error and no plan digest.
|
|
208
|
+
*/
|
|
209
|
+
export function choudoufuSetPlanParts(input) {
|
|
210
|
+
const doc = isObject(input.document) ? input.document : {};
|
|
211
|
+
if (!Array.isArray(doc.roots))
|
|
212
|
+
throw new Error('not a choudoufu set plan document: no top-level "roots"');
|
|
213
|
+
return doc.roots.map((r) => {
|
|
214
|
+
const member = input.memberFor ? input.memberFor(r.root, r.estate || undefined) : r.root;
|
|
215
|
+
const scope = r.estate || undefined;
|
|
216
|
+
if (r.status === "planned" && isObject(r.plan)) {
|
|
217
|
+
return terraformChangeSetPart({ member, plan: r.plan, planner: "choudoufu", ...(scope ? { scope } : {}), ...(r.digest ? { nativeDigest: r.digest } : {}) });
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
member: {
|
|
221
|
+
member,
|
|
222
|
+
lexicon: "terraform",
|
|
223
|
+
planner: "choudoufu",
|
|
224
|
+
...(scope ? { scope } : {}),
|
|
225
|
+
status: "failed",
|
|
226
|
+
error: r.error || `the root did not plan (${r.status ?? "no status"}${r.stage ? `, at ${r.stage}` : ""})`,
|
|
227
|
+
planDigest: null,
|
|
228
|
+
...(r.digest ? { nativeDigest: r.digest } : {}),
|
|
229
|
+
holes: [],
|
|
230
|
+
},
|
|
231
|
+
entries: [],
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which `terraform.roots` a change touched (#3183), from the changed files
|
|
3
|
+
* alone.
|
|
4
|
+
*
|
|
5
|
+
* A root is hand-written HCL, not chant source, so `chant lifecycle affected`
|
|
6
|
+
* never sees it move: its artifact diff covers what chant builds. A pull
|
|
7
|
+
* request's plan needs the roots a change reaches, and this answers that
|
|
8
|
+
* from paths: a root is touched when a changed file sits in its directory,
|
|
9
|
+
* in a local module it calls (followed through modules that call modules),
|
|
10
|
+
* or is one of its var files.
|
|
11
|
+
*
|
|
12
|
+
* The module calls are read with a regex over the root's `.tf` and `.tofu`
|
|
13
|
+
* files, the way `./op/activities/live-detect.ts` reads a live block, so
|
|
14
|
+
* this stays out of the HCL parser's dependency graph. Only a `source` that
|
|
15
|
+
* starts with `./` or `../` is followed; a registry or git source is a
|
|
16
|
+
* version pin in the root's own files, which a change to it already touches.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
19
|
+
import { join, posix, relative, resolve, sep } from "node:path";
|
|
20
|
+
/** `source = "./..."` or `"../..."` inside a module block. Other sources are not on disk here. */
|
|
21
|
+
const LOCAL_SOURCE = /\bsource\s*=\s*"(\.{1,2}\/[^"]*)"/g;
|
|
22
|
+
const toPosix = (p) => p.split(sep).join("/");
|
|
23
|
+
/** `dir` relative to `projectRoot`, `/`-separated, without a trailing slash; `.` for the root itself. */
|
|
24
|
+
function projectRelative(projectRoot, dir) {
|
|
25
|
+
const rel = toPosix(relative(projectRoot, dir));
|
|
26
|
+
return rel === "" ? "." : posix.normalize(rel).replace(/\/+$/, "");
|
|
27
|
+
}
|
|
28
|
+
/** Local module directories `dir` calls, read from its HCL files. */
|
|
29
|
+
function localModuleDirs(dir) {
|
|
30
|
+
let names;
|
|
31
|
+
try {
|
|
32
|
+
names = readdirSync(dir);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const name of names) {
|
|
39
|
+
if (!name.endsWith(".tf") && !name.endsWith(".tofu"))
|
|
40
|
+
continue;
|
|
41
|
+
let text;
|
|
42
|
+
try {
|
|
43
|
+
text = readFileSync(join(dir, name), "utf8");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const m of text.matchAll(LOCAL_SOURCE)) {
|
|
49
|
+
// A `//subdir` suffix names a directory inside the source.
|
|
50
|
+
const target = resolve(dir, m[1].replace("//", "/"));
|
|
51
|
+
if (existsSync(target) && statSync(target).isDirectory())
|
|
52
|
+
out.push(target);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Every path, relative to `projectRoot`, whose change touches `root`: its
|
|
59
|
+
* directory, each local module it reaches, and its var files.
|
|
60
|
+
*/
|
|
61
|
+
export function rootWatchPaths(projectRoot, root) {
|
|
62
|
+
const rootDir = resolve(projectRoot, root.dir);
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const queue = [rootDir];
|
|
65
|
+
while (queue.length > 0) {
|
|
66
|
+
const dir = queue.shift();
|
|
67
|
+
if (seen.has(dir))
|
|
68
|
+
continue;
|
|
69
|
+
seen.add(dir);
|
|
70
|
+
queue.push(...localModuleDirs(dir));
|
|
71
|
+
}
|
|
72
|
+
const paths = [...seen].map((d) => projectRelative(projectRoot, d));
|
|
73
|
+
for (const file of root.varFiles ?? [])
|
|
74
|
+
paths.push(projectRelative(projectRoot, resolve(rootDir, file)));
|
|
75
|
+
return [...new Set(paths)].sort();
|
|
76
|
+
}
|
|
77
|
+
/** Whether `file` is `path` or sits under it. Both relative to the project, `/`-separated. */
|
|
78
|
+
function covers(path, file) {
|
|
79
|
+
if (path === ".")
|
|
80
|
+
return !file.startsWith("../");
|
|
81
|
+
return file === path || file.startsWith(`${path}/`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The names of the roots in `roots` that a change to `changedFiles` touches,
|
|
85
|
+
* sorted. `changedFiles` are relative to `projectRoot` with `/` separators,
|
|
86
|
+
* as `git diff --name-only --relative` prints them.
|
|
87
|
+
*/
|
|
88
|
+
export function changedRoots(projectRoot, roots, changedFiles) {
|
|
89
|
+
const files = changedFiles.map((f) => posix.normalize(f));
|
|
90
|
+
const touched = [];
|
|
91
|
+
for (const [name, root] of Object.entries(roots)) {
|
|
92
|
+
const watch = rootWatchPaths(projectRoot, root);
|
|
93
|
+
if (files.some((f) => watch.some((p) => covers(p, f))))
|
|
94
|
+
touched.push(name);
|
|
95
|
+
}
|
|
96
|
+
return touched.sort();
|
|
97
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { dirname, join } from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { docsPipeline, writeDocsSite } from "@intentius/chant/codegen/docs";
|
|
4
|
+
const overview = `
|
|
5
|
+
The Terraform lexicon reads an estate's existing \`.tf\` files. There is no
|
|
6
|
+
generated resource surface here and nothing is emitted back over the HCL:
|
|
7
|
+
Terraform's types live in provider registries, one schema per provider and
|
|
8
|
+
version, and an estate that already has a root module does not need a second,
|
|
9
|
+
generated copy of it.
|
|
10
|
+
|
|
11
|
+
What you declare is the directory. Each entry in \`terraform.roots\` names a
|
|
12
|
+
root module; \`buildRoots()\` parses it at build time and every HCL block joins
|
|
13
|
+
the build as an entity keyed \`<root>/<address>\`, carrying the block body
|
|
14
|
+
verbatim. Those entities are what the post-synth checks and \`chant audit\`
|
|
15
|
+
read.
|
|
16
|
+
|
|
17
|
+
See [Getting Started](getting-started/) for the config shape, the block-to-entity
|
|
18
|
+
mapping, and what TF001 reports.
|
|
19
|
+
`;
|
|
20
|
+
/**
|
|
21
|
+
* Generate the docs site for the terraform lexicon.
|
|
22
|
+
*
|
|
23
|
+
* `basePath` is what the unified site at /chant needs so assets and internal
|
|
24
|
+
* links resolve once this site is copied under it (scripts/build-docs.test.ts
|
|
25
|
+
* asserts it). Authored pages come from `docs/pages/` by default.
|
|
26
|
+
*/
|
|
27
|
+
export async function generateDocs(options) {
|
|
28
|
+
const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
29
|
+
const config = {
|
|
30
|
+
name: "terraform",
|
|
31
|
+
displayName: "Terraform",
|
|
32
|
+
description: "Existing Terraform root modules as declarable chant entities",
|
|
33
|
+
distDir: join(pkgDir, "dist"),
|
|
34
|
+
outDir: join(pkgDir, "docs"),
|
|
35
|
+
basePath: process.env.DOCS_BASE_PATH ?? "/chant/lexicons/terraform/",
|
|
36
|
+
overview,
|
|
37
|
+
serviceFromType: (type) => type.split("::")[1] ?? type,
|
|
38
|
+
};
|
|
39
|
+
const result = docsPipeline(config);
|
|
40
|
+
writeDocsSite(config, result);
|
|
41
|
+
if (options?.verbose) {
|
|
42
|
+
console.error(`Generated ${result.pages.size} documentation pages`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { generate, writeGeneratedFiles } from "./generate.js";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
// src/codegen/generate-cli.ts -> src/codegen -> src -> package root.
|
|
6
|
+
// Two dirnames land in src/ and the first generate writes src/src/generated/ (#1614).
|
|
7
|
+
const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
8
|
+
const result = await generate({ verbose: true });
|
|
9
|
+
writeGeneratedFiles(result, pkgDir);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generation for the terraform lexicon: there is nothing to generate.
|
|
3
|
+
*
|
|
4
|
+
* Every other lexicon fetches one upstream schema and emits typed resource
|
|
5
|
+
* classes from it. Terraform has no such thing. Its resource surface lives in
|
|
6
|
+
* provider registries, one schema per provider and per version, and this
|
|
7
|
+
* lexicon does not model it: an estate's `.tf` files are read as they are
|
|
8
|
+
* (see `../hcl/parse.ts`) and the blocks become entities with their bodies
|
|
9
|
+
* carried verbatim. So `core`'s `generatePipeline` is not called at all, and
|
|
10
|
+
* this returns the empty result the packaging pipeline needs to assemble a
|
|
11
|
+
* bundle.
|
|
12
|
+
*
|
|
13
|
+
* The registry is still written, empty, because `validate` reads it and
|
|
14
|
+
* `writeBundleSpec` derives `dist/meta.json` from it. An empty registry is the
|
|
15
|
+
* honest statement that this lexicon exports no generated resource classes.
|
|
16
|
+
*/
|
|
17
|
+
import { writeGeneratedArtifacts } from "@intentius/chant/codegen/generate";
|
|
18
|
+
import { dirname } from "path";
|
|
19
|
+
import { fileURLToPath } from "url";
|
|
20
|
+
/** Filename of the generated registry, read back by `../validate.ts`. */
|
|
21
|
+
export const LEXICON_JSON_FILENAME = "lexicon-terraform.json";
|
|
22
|
+
const EMPTY_INDEX_TS = `// Code generated by chant generate. DO NOT EDIT.
|
|
23
|
+
// The terraform lexicon models no upstream schema, so there are no resource
|
|
24
|
+
// classes to export. See src/codegen/generate.ts.
|
|
25
|
+
export {};
|
|
26
|
+
`;
|
|
27
|
+
const EMPTY_TYPES_DTS = `// Code generated by chant generate. DO NOT EDIT.
|
|
28
|
+
export {};
|
|
29
|
+
`;
|
|
30
|
+
export async function generate(options) {
|
|
31
|
+
const result = {
|
|
32
|
+
lexiconJSON: `${JSON.stringify({}, null, 2)}\n`,
|
|
33
|
+
typesDTS: EMPTY_TYPES_DTS,
|
|
34
|
+
indexTS: EMPTY_INDEX_TS,
|
|
35
|
+
resources: 0,
|
|
36
|
+
properties: 0,
|
|
37
|
+
enums: 0,
|
|
38
|
+
warnings: [],
|
|
39
|
+
};
|
|
40
|
+
if (options?.verbose) {
|
|
41
|
+
console.error("terraform generates no types: the .tf files an estate already has are the input");
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Write generated files to the package directory.
|
|
47
|
+
*/
|
|
48
|
+
export function writeGeneratedFiles(result, pkgDir) {
|
|
49
|
+
// This file lives in src/codegen/, so the package root is three levels up.
|
|
50
|
+
const dir = pkgDir ?? dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
51
|
+
writeGeneratedArtifacts({
|
|
52
|
+
baseDir: dir,
|
|
53
|
+
files: {
|
|
54
|
+
[LEXICON_JSON_FILENAME]: result.lexiconJSON,
|
|
55
|
+
"index.d.ts": result.typesDTS,
|
|
56
|
+
"index.ts": result.indexTS,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { moduleSourceDir } from "@intentius/chant/module-source";
|
|
4
|
+
import { packagePipeline, collectSkills } from "@intentius/chant/codegen/package";
|
|
5
|
+
import { generate } from "./generate.js";
|
|
6
|
+
import { terraformPlugin } from "../plugin.js";
|
|
7
|
+
/**
|
|
8
|
+
* This package's `src/` directory. `packagePipeline` scans it for the rule
|
|
9
|
+
* files that go into `dist/rules/`, so it must be `src/` and not `src/codegen/`
|
|
10
|
+
* (which is where `dirname(import.meta.url)` alone lands, and where
|
|
11
|
+
* `lint/rules` does not exist). `lexicons/aws/src/codegen/package.ts` resolves
|
|
12
|
+
* it the same way.
|
|
13
|
+
*/
|
|
14
|
+
const srcDir = dirname(moduleSourceDir(import.meta.url));
|
|
15
|
+
/** The published version, so the manifest cannot drift from package.json. */
|
|
16
|
+
function packageVersion() {
|
|
17
|
+
try {
|
|
18
|
+
const pkg = JSON.parse(readFileSync(join(srcDir, "..", "package.json"), "utf-8"));
|
|
19
|
+
return pkg.version ?? "0.0.0";
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return "0.0.0";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Package the terraform lexicon for distribution.
|
|
27
|
+
*/
|
|
28
|
+
export async function packageLexicon(options) {
|
|
29
|
+
const version = packageVersion();
|
|
30
|
+
const { spec, stats } = await packagePipeline({
|
|
31
|
+
generate: (opts) => generate({ verbose: opts?.verbose, force: opts?.force }),
|
|
32
|
+
buildManifest: () => ({
|
|
33
|
+
name: "terraform",
|
|
34
|
+
version,
|
|
35
|
+
chantVersion: ">=0.1.0",
|
|
36
|
+
namespace: "Terraform",
|
|
37
|
+
}),
|
|
38
|
+
srcDir,
|
|
39
|
+
collectSkills: () => collectSkills(terraformPlugin.skills?.() ?? []),
|
|
40
|
+
version,
|
|
41
|
+
}, options);
|
|
42
|
+
return { spec, stats };
|
|
43
|
+
}
|
package/dist/commands.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant terraform pin-rollout`: one run of a pin-bump rollout (#3189), from
|
|
3
|
+
* the command line. The `TerraformPinRolloutOp` composite runs the same code
|
|
4
|
+
* as an Op step.
|
|
5
|
+
*
|
|
6
|
+
* chant terraform pin-rollout --module <source> --from <pin> --to <pin>
|
|
7
|
+
* [--root <dir>]... [--depends-on <dir>=<dir>]... [--ts-source <dir>=<file>]...
|
|
8
|
+
* [--canary <dir>]... [--waves-from <live-waves.json> [--waves-prefix <dir>]]
|
|
9
|
+
* [--pull-request] [--base <branch>] [--remote <name>] [--applied-check <name>] [--json]
|
|
10
|
+
*
|
|
11
|
+
* Without `--pull-request` it reports the next wave and opens nothing. Exit
|
|
12
|
+
* 0 when the rollout is complete or a wave's PR was opened (or would be), 3
|
|
13
|
+
* when it is waiting on a merge or an apply (a gate is a fact, #2119), and 1
|
|
14
|
+
* when it stopped on a failed root or a closed PR.
|
|
15
|
+
*/
|
|
16
|
+
import { splitJoinedFlags, unknownFlagError } from "@intentius/chant/cli/command-group";
|
|
17
|
+
const USAGE = "usage: chant terraform pin-rollout --module <source> --from <pin> --to <pin> [--root <dir>]... [--depends-on <dir>=<dir>]... " +
|
|
18
|
+
"[--ts-source <dir>=<file>]... [--canary <dir>]... [--waves-from <file> [--waves-prefix <dir>]] [--pull-request] " +
|
|
19
|
+
"[--base <branch>] [--remote <name>] [--applied-check <name>] [--json]";
|
|
20
|
+
const VALUE_FLAGS = new Set(["--module", "--from", "--to", "--root", "--depends-on", "--ts-source", "--canary", "--waves-from", "--waves-prefix", "--base", "--remote", "--applied-check"]);
|
|
21
|
+
const BOOLEAN_FLAGS = new Set(["--pull-request", "--json"]);
|
|
22
|
+
/** Parse the verb's flags into the activity's args. Throws on an unknown flag or a missing value. */
|
|
23
|
+
export function parsePinRolloutArgs(rawArgs) {
|
|
24
|
+
const tokens = splitJoinedFlags(rawArgs, BOOLEAN_FLAGS);
|
|
25
|
+
const values = new Map();
|
|
26
|
+
const flags = new Set();
|
|
27
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
28
|
+
const t = tokens[i];
|
|
29
|
+
if (BOOLEAN_FLAGS.has(t)) {
|
|
30
|
+
flags.add(t);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (!VALUE_FLAGS.has(t))
|
|
34
|
+
throw t.startsWith("-") ? unknownFlagError(t, USAGE) : new Error(`unexpected argument "${t}"\n${USAGE}`);
|
|
35
|
+
const v = tokens[++i];
|
|
36
|
+
if (v === undefined || v.startsWith("--"))
|
|
37
|
+
throw new Error(`${t} needs a value\n${USAGE}`);
|
|
38
|
+
values.set(t, [...(values.get(t) ?? []), v]);
|
|
39
|
+
}
|
|
40
|
+
const one = (flag) => values.get(flag)?.at(-1);
|
|
41
|
+
const pairs = (flag) => (values.get(flag) ?? []).map((v) => {
|
|
42
|
+
const eq = v.indexOf("=");
|
|
43
|
+
if (eq <= 0 || eq === v.length - 1)
|
|
44
|
+
throw new Error(`${flag} takes <dir>=<value>, got "${v}"`);
|
|
45
|
+
return [v.slice(0, eq), v.slice(eq + 1)];
|
|
46
|
+
});
|
|
47
|
+
for (const required of ["--module", "--from", "--to"])
|
|
48
|
+
if (!one(required))
|
|
49
|
+
throw new Error(`${required} is required\n${USAGE}`);
|
|
50
|
+
const dependsOn = pairs("--depends-on");
|
|
51
|
+
const tsSource = new Map(pairs("--ts-source"));
|
|
52
|
+
const named = [...new Set([...(values.get("--root") ?? []), ...dependsOn.map(([r]) => r), ...tsSource.keys()])];
|
|
53
|
+
const roots = named.length > 0
|
|
54
|
+
? named.map((root) => {
|
|
55
|
+
const deps = dependsOn.filter(([r]) => r === root).map(([, d]) => d);
|
|
56
|
+
return { root, ...(deps.length > 0 ? { dependsOn: deps } : {}), ...(tsSource.has(root) ? { tsSource: tsSource.get(root) } : {}) };
|
|
57
|
+
})
|
|
58
|
+
: undefined;
|
|
59
|
+
const args = {
|
|
60
|
+
module: one("--module"),
|
|
61
|
+
from: one("--from"),
|
|
62
|
+
to: one("--to"),
|
|
63
|
+
mode: flags.has("--pull-request") ? "pull-request" : "report",
|
|
64
|
+
...(roots ? { roots } : {}),
|
|
65
|
+
...(values.has("--canary") ? { canaries: values.get("--canary") } : {}),
|
|
66
|
+
...(one("--waves-from") ? { wavesFrom: one("--waves-from") } : {}),
|
|
67
|
+
...(one("--waves-prefix") ? { wavesPrefix: one("--waves-prefix") } : {}),
|
|
68
|
+
...(one("--base") ? { base: one("--base") } : {}),
|
|
69
|
+
...(one("--remote") ? { remote: one("--remote") } : {}),
|
|
70
|
+
...(one("--applied-check") ? { appliedCheck: one("--applied-check") } : {}),
|
|
71
|
+
};
|
|
72
|
+
return { args, json: flags.has("--json") };
|
|
73
|
+
}
|
|
74
|
+
/** Exit status for a rollout's status. */
|
|
75
|
+
export function pinRolloutExitCode(status) {
|
|
76
|
+
if (status === "waiting")
|
|
77
|
+
return 3;
|
|
78
|
+
if (status === "stopped")
|
|
79
|
+
return 1;
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
async function runPinRolloutVerb(ctx) {
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = parsePinRolloutArgs(ctx.rawArgs);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
const { readPinRollout } = await import("./op/activities/pin-rollout.js");
|
|
92
|
+
try {
|
|
93
|
+
const result = await readPinRollout(parsed.args);
|
|
94
|
+
console.log(parsed.json ? JSON.stringify(result, null, 2) : result.summary);
|
|
95
|
+
return pinRolloutExitCode(result.status);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export const terraformCommands = {
|
|
103
|
+
name: "terraform",
|
|
104
|
+
description: "Terraform and OpenTofu roots: roll a module version out as one pin-bump PR per wave",
|
|
105
|
+
commands: [{ name: "pin-rollout", description: "Read a pin-bump rollout's state and open the next wave's PR when it is due", handler: runPinRolloutVerb }],
|
|
106
|
+
};
|
|
@@ -20,12 +20,28 @@
|
|
|
20
20
|
* that file and nothing else. terraform and tofu refuse a saved plan whose
|
|
21
21
|
* state moved since it was written; choudoufu re-plans against the live
|
|
22
22
|
* system and refuses with exit 3 when its fresh plan differs (choudoufu
|
|
23
|
-
* #878). Either refusal fails the step.
|
|
23
|
+
* #878). Either refusal fails the step. choudoufu's refusal of a plan the
|
|
24
|
+
* wave handed `run` throws {@link TerraformPlanMovedError}, so the command
|
|
25
|
+
* that applied it exits 3 (#3997). terraform's and tofu's stale-plan error
|
|
26
|
+
* is not classified and exits 1.
|
|
24
27
|
*
|
|
25
28
|
* Run without a wave plan (`chant run --components`, or a fan-out with no
|
|
26
29
|
* wave gate), `run` plans and applies in one go, which is what `terraform
|
|
27
30
|
* apply` with no plan file does too.
|
|
28
31
|
*
|
|
32
|
+
* ## What the plan says about the root's outputs (#3999)
|
|
33
|
+
*
|
|
34
|
+
* `plan` answers the outputs the root will have once that plan applies
|
|
35
|
+
* (`terraformPlannedOutputs`): `show -json`'s `planned_values.outputs`, less
|
|
36
|
+
* each output whose `output_changes` entry is unknown after apply. A pull
|
|
37
|
+
* request's plan reads a dependent's `stackOutput()` references from there
|
|
38
|
+
* when both roots are in the change.
|
|
39
|
+
*
|
|
40
|
+
* A root whose `vars` read an output not known yet (`DeployContext.unresolvedInputs`)
|
|
41
|
+
* is not planned: terraform takes no unknown variable. Its member gets a hole
|
|
42
|
+
* naming the roots it waits on and no artifact, and the run that applies it
|
|
43
|
+
* plans it once their outputs are known.
|
|
44
|
+
*
|
|
29
45
|
* ## choudoufu roots in a gated wave (#3459)
|
|
30
46
|
*
|
|
31
47
|
* A live choudoufu root that `live-plan-set` can plan as chant would (no
|
|
@@ -48,7 +64,7 @@
|
|
|
48
64
|
* chant's waves stay the order. Each root is its own one-root set, so
|
|
49
65
|
* choudoufu's wave over it is always wave 1.
|
|
50
66
|
*/
|
|
51
|
-
import type { Capability } from "@intentius/chant/components/capability";
|
|
67
|
+
import type { Capability, PlannedOutputs } from "@intentius/chant/components/capability";
|
|
52
68
|
import { type CapabilityPlugin } from "@intentius/chant/components/capability-plugin";
|
|
53
69
|
/** The `terraform-apply` step's input. */
|
|
54
70
|
export interface TerraformApplyStepInput {
|
|
@@ -68,6 +84,26 @@ export interface TerraformApplyStepOutput {
|
|
|
68
84
|
/** `terraform output -json`, as `{ name: value }`. */
|
|
69
85
|
outputs: Record<string, unknown>;
|
|
70
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The outputs a root will have once the plan `json` (`show -json`) applies
|
|
89
|
+
* (#3999). An output is known when `planned_values.outputs` gives its value
|
|
90
|
+
* and its `output_changes` entry marks no part of it unknown after apply.
|
|
91
|
+
* Every other output the plan names is unknown, the whole output even when
|
|
92
|
+
* only a part of it is.
|
|
93
|
+
*/
|
|
94
|
+
export declare function terraformPlannedOutputs(json: unknown): PlannedOutputs;
|
|
95
|
+
/**
|
|
96
|
+
* choudoufu refused the plan `run` was handed by its wave (its exit 3):
|
|
97
|
+
* the live system moved since the plan, or the plan names another estate.
|
|
98
|
+
* Nothing was applied. `planMoved` is core's mark for a moved plan
|
|
99
|
+
* (`isPlanMovedError`), so the command that applied it exits 3, as for the
|
|
100
|
+
* other capabilities that refuse a moved plan (#3997).
|
|
101
|
+
*/
|
|
102
|
+
export declare class TerraformPlanMovedError extends Error {
|
|
103
|
+
readonly root: string;
|
|
104
|
+
readonly planMoved = true;
|
|
105
|
+
constructor(root: string, message: string);
|
|
106
|
+
}
|
|
71
107
|
export declare const terraformApplyCapability: Capability<TerraformApplyStepInput, TerraformApplyStepOutput>;
|
|
72
108
|
export declare const TERRAFORM_VERB_FAMILIES: {
|
|
73
109
|
readonly apply: readonly ["terraform-apply"];
|