@intentius/chant-lexicon-terraform 0.124.31 → 0.124.32
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.js +152 -0
- package/dist/composites/terraform-apply-op.js +183 -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/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,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TerraformWatchOp composite (#2087): drift reporting for one root module,
|
|
3
|
+
* on a schedule.
|
|
4
|
+
*
|
|
5
|
+
* The observe position on the lifecycle dial. `TerraformApplyOp` next door
|
|
6
|
+
* changes the estate; this one never does: it inits, plans, and reports what
|
|
7
|
+
* the plan found. `terraform plan -detailed-exitcode` is the whole mechanism.
|
|
8
|
+
* Exit 0 is "the world matches the configuration", exit 2 is drift, and
|
|
9
|
+
* `terraformPlan` turns that into the boolean `changed`, which rides out of
|
|
10
|
+
* the Op as the run's `Drift` outcome.
|
|
11
|
+
*
|
|
12
|
+
* Phases: Init, Plan, and (for a finding mode that opens something) Report.
|
|
13
|
+
*
|
|
14
|
+
* ## Only the human plan is ever posted
|
|
15
|
+
*
|
|
16
|
+
* `terraformPlan` returns two renders of the same plan: `json`, from
|
|
17
|
+
* `terraform show -json`, and `text`, from `terraform show -no-color`. The
|
|
18
|
+
* JSON one carries every resource attribute value the plan touches, including
|
|
19
|
+
* provider credentials that no `sensitive` marking covers, because
|
|
20
|
+
* `sensitive_values` describes the configuration's own declarations rather
|
|
21
|
+
* than everything a provider puts in a plan. So the issue and pull-request
|
|
22
|
+
* modes reference `plan.out.text` and nothing else. There is no option to
|
|
23
|
+
* post the JSON, and `terraform-watch-op.test.ts` asserts no reference to it
|
|
24
|
+
* reaches the finding step.
|
|
25
|
+
*
|
|
26
|
+
* The same rule holds on a live root, where the JSON is choudoufu's own
|
|
27
|
+
* bound/omissions/unowned document rather than a plan representation, and
|
|
28
|
+
* carries live identities and tag values for every resource the estate
|
|
29
|
+
* touched. `choudoufuLivePlan` composes the plan text and the adoption ledger
|
|
30
|
+
* into one `finding` string for exactly this reason: the body is one
|
|
31
|
+
* reference to one text field, so there is no arrangement of the args in
|
|
32
|
+
* which the document reaches a body.
|
|
33
|
+
*
|
|
34
|
+
* ## A live root reports three things, not one (#2105)
|
|
35
|
+
*
|
|
36
|
+
* `live: true` swaps the Plan step for `choudoufuLivePlan`. On a choudoufu
|
|
37
|
+
* root ownership is a pair of tags on the resource rather than an entry in a
|
|
38
|
+
* state file, so a plan against the live system can answer three questions
|
|
39
|
+
* where a stock plan answers one: whether the estate drifted (the
|
|
40
|
+
* `-detailed-exitcode` exit, `Drift` as before), how many live resources sit
|
|
41
|
+
* at a declared identity carrying no marker (`Unowned`), and how many of those
|
|
42
|
+
* an exact content match makes claimable (`Adoptable`). All three come off a
|
|
43
|
+
* single live read, published as three run outcomes from the one step.
|
|
44
|
+
*
|
|
45
|
+
* The finding modes then carry the adoption ledger under the plan text: one
|
|
46
|
+
* line per adoptable match with its address, its live identity and the
|
|
47
|
+
* `tofu-estate`/`tofu-address` values that claim it, in the row form
|
|
48
|
+
* `live-plan -adoption-only` prints, followed by any contested address. That
|
|
49
|
+
* is a report, not an action. `TerraformAdoptOp` next door is what writes the
|
|
50
|
+
* markers, and it gates first.
|
|
51
|
+
*
|
|
52
|
+
* ## Findings reuse `reconcilePr`
|
|
53
|
+
*
|
|
54
|
+
* `issue`, `comment` and `pull-request` call core's `reconcilePr` activity,
|
|
55
|
+
* the one place in chant that shells to `gh`, rather than growing a second
|
|
56
|
+
* copy of those calls here.
|
|
57
|
+
* That activity built its own change-set summary and had no way to be handed
|
|
58
|
+
* one, so #2087 added a single field to it (`ReconcilePrArgs.body`), which is
|
|
59
|
+
* what carries the `-no-color` plan through. Note that the pull-request mode
|
|
60
|
+
* of `reconcilePr` regenerates chant TypeScript via `chant import`; a
|
|
61
|
+
* terraform-native regeneration of HCL from live state is #2089, so
|
|
62
|
+
* `findingMode: "issue"` is the mode with an end-to-end answer and
|
|
63
|
+
* `"pull-request"` opens a PR whose body is the plan. `"comment"` (#2231) is
|
|
64
|
+
* the mode for the plan-on-pull-request shape: the plan is posted onto the
|
|
65
|
+
* pull request that triggered the run and re-edited there on the next push,
|
|
66
|
+
* which is where a reviewer is already looking. It needs that trigger; a run
|
|
67
|
+
* without one fails the Report step by name rather than posting elsewhere. On a live root #2089's
|
|
68
|
+
* question has a different answer rather than a pending one, and it is not
|
|
69
|
+
* regeneration: see `TerraformAdoptOp`.
|
|
70
|
+
*
|
|
71
|
+
* ## The schedule
|
|
72
|
+
*
|
|
73
|
+
* A cron lands on the Op itself as `schedule` (#2120), the same field
|
|
74
|
+
* `WatchOp` and `ConvergeOp` set. It is runtime-neutral data: `chant
|
|
75
|
+
* operator` reads it as this Op's tick cadence, `generateOpsPipeline` against
|
|
76
|
+
* the github lexicon renders it as a CI cron, and a hosting lexicon hands it
|
|
77
|
+
* to its own scheduler. The one-shot `chant run app-watch` path ignores it.
|
|
78
|
+
* `examples/scheduled-watch/` is the CI recipe.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* import { TerraformWatchOp } from "@intentius/chant-lexicon-terraform";
|
|
83
|
+
*
|
|
84
|
+
* // one-shot, local executor: chant run app-watch
|
|
85
|
+
* export const { op } = TerraformWatchOp({ name: "app-watch", root: "app" });
|
|
86
|
+
*
|
|
87
|
+
* // the plan as a sticky comment on the pull request that triggered the run
|
|
88
|
+
* export const { op } = TerraformWatchOp({
|
|
89
|
+
* name: "app-plan",
|
|
90
|
+
* root: "app",
|
|
91
|
+
* findingMode: "comment",
|
|
92
|
+
* });
|
|
93
|
+
*
|
|
94
|
+
* // nightly, opening an issue when the plan is non-empty
|
|
95
|
+
* export const { op } = TerraformWatchOp({
|
|
96
|
+
* name: "app-watch",
|
|
97
|
+
* root: "app",
|
|
98
|
+
* schedule: "0 6 * * *",
|
|
99
|
+
* findingMode: "issue",
|
|
100
|
+
* });
|
|
101
|
+
*
|
|
102
|
+
* // a choudoufu estate: drift, unowned and adoptable, with the ledger in the issue
|
|
103
|
+
* export const { op } = TerraformWatchOp({
|
|
104
|
+
* name: "estate-watch",
|
|
105
|
+
* root: "estate",
|
|
106
|
+
* live: true,
|
|
107
|
+
* findingMode: "issue",
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
import { Op, phase } from "@intentius/chant/op";
|
|
112
|
+
import { resolveRootModeSync } from "../op/resolve-root-mode.js";
|
|
113
|
+
import { CHOUDOUFU_LIVE_PLAN_OUT_REFUSAL, DEFAULT_PLAN_FILE } from "../op/activities/terraform.js";
|
|
114
|
+
import { terraformInit as initStep, terraformPlan as planStep, choudoufuLivePlan as livePlanStep, } from "../op/builders.js";
|
|
115
|
+
export function TerraformWatchOp(config) {
|
|
116
|
+
const findingMode = config.findingMode ?? "report";
|
|
117
|
+
const where = config.cwd ? { cwd: config.cwd } : {};
|
|
118
|
+
if (config.live && config.planFile !== undefined) {
|
|
119
|
+
throw new Error(`TerraformWatchOp "${config.name}": planFile is refused on a live root. choudoufu: ` +
|
|
120
|
+
`"${CHOUDOUFU_LIVE_PLAN_OUT_REFUSAL}". A live plan writes no saved plan, so there is no file to name; ` +
|
|
121
|
+
"drop planFile, or drop live if this root runs stock.");
|
|
122
|
+
}
|
|
123
|
+
if (!config.live && config.estate !== undefined) {
|
|
124
|
+
throw new Error(`TerraformWatchOp "${config.name}": estate names the estate a live plan looks for markers of, and ` +
|
|
125
|
+
"this Op is not on a live root. Set live: true, or drop estate.");
|
|
126
|
+
}
|
|
127
|
+
// The `live` flag against the root it names. Best-effort and synchronous:
|
|
128
|
+
// `resolveRootModeSync` reads a `chant.config.json` only and returns
|
|
129
|
+
// undefined for anything else, so a `chant.config.ts` project reaches
|
|
130
|
+
// neither branch and TF028 is what reports the mismatch there. Where the
|
|
131
|
+
// mode does resolve, refusing at build time names the Op, which a
|
|
132
|
+
// post-synth diagnostic does too but a run-time failure at the first step
|
|
133
|
+
// does not.
|
|
134
|
+
const resolvedMode = resolveRootModeSync(config.root, config.cwd)?.mode;
|
|
135
|
+
if (resolvedMode === "live" && !config.live) {
|
|
136
|
+
throw new Error(`TerraformWatchOp "${config.name}": root "${config.root}" runs choudoufu with a declared estate, ` +
|
|
137
|
+
"but this Op watches it in stock mode. A stock plan reports drift alone and stays quiet about " +
|
|
138
|
+
"every unowned and adoptable resource in the estate. Set live: true.");
|
|
139
|
+
}
|
|
140
|
+
if (resolvedMode === "state" && config.live) {
|
|
141
|
+
throw new Error(`TerraformWatchOp "${config.name}": live: true, but root "${config.root}" declares no estate (or ` +
|
|
142
|
+
"terraform.binary is not \"choudoufu\"), so there is no live system to plan against. Drop " +
|
|
143
|
+
"live, or declare the root's estate.");
|
|
144
|
+
}
|
|
145
|
+
// `id` is what makes `plan.out` legal, and one field of `plan.out` is the
|
|
146
|
+
// only channel by which any part of the plan leaves this Op.
|
|
147
|
+
//
|
|
148
|
+
// A live root reads the live system instead of a state file, so the three
|
|
149
|
+
// things a watch reports come off one `choudoufuLivePlan`: `drift` from
|
|
150
|
+
// `-detailed-exitcode`, and `unowned`/`adoptable` from the `-json` document
|
|
151
|
+
// the same run wrote. Three attributes, one live read: see
|
|
152
|
+
// `ActivityStep.outcomeAttribute`'s array form (#2105).
|
|
153
|
+
const plan = config.live
|
|
154
|
+
? livePlanStep(config.root, { ...where, ...(config.estate ? { estate: config.estate } : {}), id: "plan" })
|
|
155
|
+
: planStep(config.root, { planFile: config.planFile ?? DEFAULT_PLAN_FILE, ...where, id: "plan" });
|
|
156
|
+
plan.outcomeAttribute = config.live
|
|
157
|
+
? [
|
|
158
|
+
{ name: "Drift", from: "drift" },
|
|
159
|
+
{ name: "Unowned", from: "unowned" },
|
|
160
|
+
{ name: "Adoptable", from: "adoptable" },
|
|
161
|
+
]
|
|
162
|
+
: { name: "Drift", from: "changed" };
|
|
163
|
+
const phases = [
|
|
164
|
+
phase("Init", [initStep(config.root, { ...where, ...(config.upgrade ? { upgrade: true } : {}) })]),
|
|
165
|
+
phase("Plan", [plan]),
|
|
166
|
+
];
|
|
167
|
+
if (findingMode !== "report") {
|
|
168
|
+
// `env` is `reconcilePr`'s name for the thing being reconciled; for
|
|
169
|
+
// terraform that is the root, which with its workspace is the deployment
|
|
170
|
+
// target. `entries: []` and `body` together mean the activity opens what
|
|
171
|
+
// it is given rather than running `chant lifecycle plan` to find out.
|
|
172
|
+
//
|
|
173
|
+
// `op` is what keeps the sticky issue this Op's own (#2319). The root
|
|
174
|
+
// alone does not: a stock drift watch and a `live: true` watch over one
|
|
175
|
+
// root is the pairing this composite is built for, and before #2319 both
|
|
176
|
+
// resolved to one env-keyed marker, so each nightly run rewrote the
|
|
177
|
+
// other's issue title and body. Two Ops in a project do not share a raw
|
|
178
|
+
// name — it names the Op's output directory — though the marker carries
|
|
179
|
+
// the slug rather than the raw name, and the slug is not injective; see
|
|
180
|
+
// `issueMarker` for what that leaves and why it is accepted.
|
|
181
|
+
const report = {
|
|
182
|
+
kind: "activity",
|
|
183
|
+
fn: "reconcilePr",
|
|
184
|
+
args: {
|
|
185
|
+
env: config.root,
|
|
186
|
+
op: config.name,
|
|
187
|
+
mode: findingMode,
|
|
188
|
+
entries: [],
|
|
189
|
+
title: config.title ?? `Terraform drift in root "${config.root}"`,
|
|
190
|
+
// Stock: the `-no-color` plan. Live: the same plan text with the
|
|
191
|
+
// adoption ledger under it, which `choudoufuLivePlan` composes into
|
|
192
|
+
// one `finding` field precisely so that a body is one reference and
|
|
193
|
+
// the `-json` document has no path into one at all.
|
|
194
|
+
body: config.live ? plan.out.finding : plan.out.text,
|
|
195
|
+
...(config.branch ? { branch: config.branch } : {}),
|
|
196
|
+
},
|
|
197
|
+
outcomeAttribute: findingMode === "pull-request"
|
|
198
|
+
? { name: "PR", from: "prUrl" }
|
|
199
|
+
: findingMode === "comment"
|
|
200
|
+
? { name: "Comment", from: "commentUrl" }
|
|
201
|
+
: { name: "Issue", from: "issueUrl" },
|
|
202
|
+
};
|
|
203
|
+
phases.push(phase("Report", [report]));
|
|
204
|
+
}
|
|
205
|
+
const op = Op({
|
|
206
|
+
name: config.name,
|
|
207
|
+
overview: config.live
|
|
208
|
+
? `Live-plan the "${config.root}" choudoufu estate and report drift, unowned and adoptable resources`
|
|
209
|
+
: `Plan the "${config.root}" terraform root and report drift`,
|
|
210
|
+
labels: {
|
|
211
|
+
Watch: "true",
|
|
212
|
+
TerraformRoot: config.root,
|
|
213
|
+
...(config.live ? { TerraformMode: "live" } : {}),
|
|
214
|
+
},
|
|
215
|
+
...(config.schedule ? { schedule: { cron: config.schedule, overlap: "skip" } } : {}),
|
|
216
|
+
phases,
|
|
217
|
+
});
|
|
218
|
+
return { op };
|
|
219
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `terraform` namespace in `chant.config.ts` (#1344).
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import type { ChantConfig } from "@intentius/chant/config";
|
|
6
|
+
* import "@intentius/chant-lexicon-terraform"; // brings the `terraform` key into ChantConfig
|
|
7
|
+
*
|
|
8
|
+
* export default {
|
|
9
|
+
* lexicons: ["terraform"],
|
|
10
|
+
* terraform: {
|
|
11
|
+
* binary: "tofu",
|
|
12
|
+
* roots: {
|
|
13
|
+
* app: { dir: "./terraform/app", workspace: "prod", varFiles: ["prod.tfvars"] },
|
|
14
|
+
* },
|
|
15
|
+
* },
|
|
16
|
+
* } satisfies ChantConfig;
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* One schema, three consumers: core validates the namespace against it at load,
|
|
20
|
+
* the type below is derived from it, and that derived type is what augments
|
|
21
|
+
* `ChantConfig`, so an unknown key fails at build time and at compile time for
|
|
22
|
+
* the same reason, rather than being silently ignored at both.
|
|
23
|
+
*/
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
/**
|
|
26
|
+
* chant's cross-lexicon delete-mode vocabulary (`lexicons/k8s/src/op/
|
|
27
|
+
* activities/kubectl.ts`'s `ApplyDeleteMode`, `packages/core/src/op/
|
|
28
|
+
* activities/apply.ts`'s `DeleteMode`), read here for a live root and mapped
|
|
29
|
+
* onto choudoufu's `policy` block (#2106): `"never"` requires the root's
|
|
30
|
+
* `policy` to set `undeclared_tagged` to `"keep"`, `"untag"` or `"report"`
|
|
31
|
+
* (TF026 enforces this at build time); `"owned-only"` is choudoufu's own
|
|
32
|
+
* default verb for that quadrant (`delete`) and needs nothing; `"gated"`
|
|
33
|
+
* needs nothing beyond `TerraformApplyOp`'s own approval gate. Inert on a
|
|
34
|
+
* stock root: choudoufu's `policy` block does not exist there.
|
|
35
|
+
*/
|
|
36
|
+
export const terraformDeleteModeSchema = z.enum(["never", "owned-only", "gated"]);
|
|
37
|
+
/**
|
|
38
|
+
* One Terraform root module chant reads. `dir` is the only required field:
|
|
39
|
+
* everything else names how the root is invoked rather than what it declares.
|
|
40
|
+
*
|
|
41
|
+
* `strictObject`, not `object`. Core applies `.strict()` to the top level of a
|
|
42
|
+
* declared namespace itself, but nested objects are the lexicon's own to make
|
|
43
|
+
* strict, and a root entry is exactly where a typo (`varfiles`) would
|
|
44
|
+
* otherwise land silently.
|
|
45
|
+
*/
|
|
46
|
+
export const terraformRootSchema = z.strictObject({
|
|
47
|
+
/** Root module directory, relative to the project root (where `chant.config.*` lives). */
|
|
48
|
+
dir: z.string(),
|
|
49
|
+
/** Terraform workspace to select for this root. Omitted means `default`. */
|
|
50
|
+
workspace: z.string().optional(),
|
|
51
|
+
/** `-var-file` arguments, in order, relative to `dir`. */
|
|
52
|
+
varFiles: z.array(z.string()).optional(),
|
|
53
|
+
/** `-backend-config` key/value pairs handed to `init`. */
|
|
54
|
+
backendConfig: z.record(z.string(), z.string()).optional(),
|
|
55
|
+
/**
|
|
56
|
+
* The choudoufu estate this root runs against, for a live root whose HCL
|
|
57
|
+
* does not declare one of its own (#2479).
|
|
58
|
+
*
|
|
59
|
+
* This is the environment dimension. A live root has no backend and no
|
|
60
|
+
* state file, so nothing about it is positional: dev, staging and
|
|
61
|
+
* production can be the same directory under different estates, which is
|
|
62
|
+
* why named roots rather than a directory tree are where environments
|
|
63
|
+
* belong here.
|
|
64
|
+
*
|
|
65
|
+
* roots: {
|
|
66
|
+
* "app-staging": { dir: "./terraform", estate: "app-staging", varFiles: ["staging.tfvars"] },
|
|
67
|
+
* "app-prod": { dir: "./terraform", estate: "app-prod", varFiles: ["prod.tfvars"] },
|
|
68
|
+
* }
|
|
69
|
+
*
|
|
70
|
+
* How inputs vary across those environments is `varFiles`, which is
|
|
71
|
+
* already per-root and ordered - the question a directory tree answers
|
|
72
|
+
* with HCL inheritance, answered here by a map entry.
|
|
73
|
+
*
|
|
74
|
+
* Refused (TF028) when the root's own HCL declares an estate in a `live`
|
|
75
|
+
* block or `estate.chdf.hcl` sidecar. choudoufu refuses `-estate` beside a
|
|
76
|
+
* declaration ("This configuration's live block is what names its estate"),
|
|
77
|
+
* and a value here that the declaration then overrode would be a setting
|
|
78
|
+
* written down and silently ignored.
|
|
79
|
+
*/
|
|
80
|
+
estate: z.string().optional(),
|
|
81
|
+
/** This root's declared delete mode. See {@link terraformDeleteModeSchema}. */
|
|
82
|
+
delete: terraformDeleteModeSchema.optional(),
|
|
83
|
+
});
|
|
84
|
+
export const terraformConfigSchema = z.strictObject({
|
|
85
|
+
/**
|
|
86
|
+
* Which CLI drives the roots. `terraform` and `tofu` are wire-compatible
|
|
87
|
+
* for everything this lexicon does, so the choice is recorded rather than
|
|
88
|
+
* inferred. `choudoufu` (#2103) is an OpenTofu fork: a root running it also
|
|
89
|
+
* needs a declared estate (a `live` block or an `estate.chdf.hcl` sidecar,
|
|
90
|
+
* seen by the shared HCL parse) before it counts as live. Declaring the
|
|
91
|
+
* binary alone runs the root stock, exactly as `terraform`/`tofu` do.
|
|
92
|
+
*/
|
|
93
|
+
binary: z.enum(["terraform", "tofu", "choudoufu"]).optional(),
|
|
94
|
+
/**
|
|
95
|
+
* How far a root's parse follows its `module` calls (#2112), spelled the
|
|
96
|
+
* way tflint spells it. `local` (the default) reads modules sourced from a
|
|
97
|
+
* relative path, so TF014, TF015 and TF020 see inside them; `none` reads
|
|
98
|
+
* only each root's own directory; `all` is reserved for fetching registry
|
|
99
|
+
* and git modules and is refused with a message, since chant fetches
|
|
100
|
+
* nothing. See `hcl/descend.ts`.
|
|
101
|
+
*/
|
|
102
|
+
callModuleType: z.enum(["local", "none", "all"]).optional(),
|
|
103
|
+
/**
|
|
104
|
+
* The directory a root's local module sources may resolve within (#2874),
|
|
105
|
+
* absolute or relative to the project root. Unset, it is the project root,
|
|
106
|
+
* or a root's own directory when its `dir` lies outside the project. A
|
|
107
|
+
* source that resolves outside it is refused with a warning, as before.
|
|
108
|
+
*
|
|
109
|
+
* `chant workspace graph` sets it to the workspace root when it reads a
|
|
110
|
+
* `terraform` or `choudoufu` member through a reader project of its own,
|
|
111
|
+
* so a member that calls `../modules/x` keeps its modules. A project
|
|
112
|
+
* rarely needs it: widening the boundary means reading modules the
|
|
113
|
+
* project does not own.
|
|
114
|
+
*/
|
|
115
|
+
moduleRoot: z.string().optional(),
|
|
116
|
+
/** Named root modules. The name is the entity-key prefix, so keep it stable. */
|
|
117
|
+
roots: z.record(z.string(), terraformRootSchema),
|
|
118
|
+
});
|