@intentius/chant-lexicon-terraform 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -0
- package/dist/codegen/docs-cli.d.ts +3 -0
- package/dist/codegen/docs-cli.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts +11 -0
- package/dist/codegen/docs.d.ts.map +1 -0
- package/dist/codegen/generate-cli.d.ts +3 -0
- package/dist/codegen/generate-cli.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts +28 -0
- package/dist/codegen/generate.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +17 -0
- package/dist/codegen/package.d.ts.map +1 -0
- package/dist/composites/terraform-apply-op.d.ts +88 -0
- package/dist/composites/terraform-apply-op.d.ts.map +1 -0
- package/dist/composites/terraform-watch-op.d.ts +124 -0
- package/dist/composites/terraform-watch-op.d.ts.map +1 -0
- package/dist/config.d.ts +83 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +127 -0
- package/dist/describe-resources.d.ts.map +1 -0
- package/dist/generated/index.d.ts +2 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/hcl/parse.d.ts +87 -0
- package/dist/hcl/parse.d.ts.map +1 -0
- package/dist/hcl/roots.d.ts +37 -0
- package/dist/hcl/roots.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/integrity.json +12 -0
- package/dist/lint/audit-catalog.d.ts +12 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -0
- package/dist/lint/audit-lineage.d.ts +21 -0
- package/dist/lint/audit-lineage.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts +3 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -0
- package/dist/lint/post-synth/tf001.d.ts +17 -0
- package/dist/lint/post-synth/tf001.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +5 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lint/rules/plan-before-apply.d.ts +30 -0
- package/dist/lint/rules/plan-before-apply.d.ts.map +1 -0
- package/dist/lsp/completions.d.ts +26 -0
- package/dist/lsp/completions.d.ts.map +1 -0
- package/dist/lsp/context.d.ts +70 -0
- package/dist/lsp/context.d.ts.map +1 -0
- package/dist/lsp/hover.d.ts +12 -0
- package/dist/lsp/hover.d.ts.map +1 -0
- package/dist/lsp/option-keys.d.ts +34 -0
- package/dist/lsp/option-keys.d.ts.map +1 -0
- package/dist/manifest.json +6 -0
- package/dist/meta.json +1 -0
- package/dist/okf/index.md +8 -0
- package/dist/okf/rules/TF001.md +11 -0
- package/dist/okf/rules/TF101.md +11 -0
- package/dist/op/activities/index.d.ts +22 -0
- package/dist/op/activities/index.d.ts.map +1 -0
- package/dist/op/activities/terraform.d.ts +213 -0
- package/dist/op/activities/terraform.d.ts.map +1 -0
- package/dist/op/builders.d.ts +56 -0
- package/dist/op/builders.d.ts.map +1 -0
- package/dist/package-cli.d.ts +3 -0
- package/dist/package-cli.d.ts.map +1 -0
- package/dist/plugin.d.ts +11 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/rules/plan-before-apply.ts +143 -0
- package/dist/rules/tf001.ts +66 -0
- package/dist/serializer.d.ts +19 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/skills/chant-terraform.md +92 -0
- package/dist/state-ownership.d.ts +27 -0
- package/dist/state-ownership.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/validate-cli.d.ts +3 -0
- package/dist/validate-cli.d.ts.map +1 -0
- package/dist/validate.d.ts +15 -0
- package/dist/validate.d.ts.map +1 -0
- package/package.json +75 -0
- package/src/__fixtures__/no-backend/main.tf +24 -0
- package/src/__fixtures__/show-state.json +68 -0
- package/src/__fixtures__/with-backend/main.tf +28 -0
- package/src/__fixtures__/with-module/main.tf +49 -0
- package/src/__fixtures__/with-module/modules/inner/main.tf +5 -0
- package/src/codegen/docs-cli.ts +4 -0
- package/src/codegen/docs.ts +50 -0
- package/src/codegen/generate-cli.ts +10 -0
- package/src/codegen/generate.ts +68 -0
- package/src/codegen/package.ts +50 -0
- package/src/composites/terraform-apply-op.acceptance.test.ts +138 -0
- package/src/composites/terraform-apply-op.test.ts +193 -0
- package/src/composites/terraform-apply-op.ts +166 -0
- package/src/composites/terraform-watch-op.test.ts +184 -0
- package/src/composites/terraform-watch-op.ts +204 -0
- package/src/config.ts +76 -0
- package/src/describe-resources.test.ts +342 -0
- package/src/describe-resources.ts +357 -0
- package/src/generated/index.d.ts +2 -0
- package/src/generated/index.ts +4 -0
- package/src/generated/lexicon-terraform.json +1 -0
- package/src/hcl/parse.ts +235 -0
- package/src/hcl/roots.ts +67 -0
- package/src/index.ts +65 -0
- package/src/lint/audit-catalog.ts +30 -0
- package/src/lint/audit-lineage.ts +21 -0
- package/src/lint/audit.test.ts +45 -0
- package/src/lint/post-synth/index.ts +7 -0
- package/src/lint/post-synth/post-synth.test.ts +95 -0
- package/src/lint/post-synth/tf001.ts +66 -0
- package/src/lint/rules/index.ts +7 -0
- package/src/lint/rules/plan-before-apply.test.ts +111 -0
- package/src/lint/rules/plan-before-apply.ts +143 -0
- package/src/lsp/completions.test.ts +120 -0
- package/src/lsp/completions.ts +101 -0
- package/src/lsp/context.test.ts +152 -0
- package/src/lsp/context.ts +349 -0
- package/src/lsp/hover.test.ts +82 -0
- package/src/lsp/hover.ts +44 -0
- package/src/lsp/option-keys.ts +106 -0
- package/src/op/activities/index.ts +48 -0
- package/src/op/activities/registry.test.ts +29 -0
- package/src/op/activities/terraform.test.ts +445 -0
- package/src/op/activities/terraform.ts +469 -0
- package/src/op/builders.test.ts +90 -0
- package/src/op/builders.ts +96 -0
- package/src/package-cli.ts +21 -0
- package/src/plugin.test.ts +271 -0
- package/src/plugin.ts +157 -0
- package/src/serializer.test.ts +26 -0
- package/src/serializer.ts +26 -0
- package/src/skills/chant-terraform.md +92 -0
- package/src/state-ownership.ts +32 -0
- package/src/validate-cli.ts +5 -0
- package/src/validate.ts +28 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* terraform Op activities (#2086) — init, plan, apply and show against a root
|
|
3
|
+
* module named in the project's `terraform.roots` config namespace (#2083).
|
|
4
|
+
*
|
|
5
|
+
* Shaped after `lexicons/k3s/src/op/activities/k3s.ts`: `promisify(exec)` with
|
|
6
|
+
* the caller's `AbortSignal` forwarded so a local timeout or Ctrl-C kills the
|
|
7
|
+
* child, `safeHeartbeat` on an interval around the long calls, and every
|
|
8
|
+
* command and environment string produced by a pure exported function so a
|
|
9
|
+
* test can assert on it without running terraform.
|
|
10
|
+
*
|
|
11
|
+
* Two invariants hold for every call:
|
|
12
|
+
*
|
|
13
|
+
* - `TF_IN_AUTOMATION=1` is in the environment, which is what tells terraform
|
|
14
|
+
* it is not talking to a person.
|
|
15
|
+
* - `-input=false` is on every command that accepts it, so a missing variable
|
|
16
|
+
* fails the step instead of blocking on a prompt nobody will answer.
|
|
17
|
+
* `terraform show` is the one command that does not accept the flag
|
|
18
|
+
* (terraform answers `flag provided but not defined: -input`), so it
|
|
19
|
+
* carries the environment variable alone.
|
|
20
|
+
*
|
|
21
|
+
* `terraformApply` takes a saved plan file and nothing else. A bare apply
|
|
22
|
+
* re-plans at apply time, which is exactly the gap an approval gate exists to
|
|
23
|
+
* close, so it is refused rather than offered.
|
|
24
|
+
*/
|
|
25
|
+
import type { TerraformConfig, TerraformRootConfig } from "../../config.js";
|
|
26
|
+
/** Plan file written into the root directory when a step names none. */
|
|
27
|
+
export declare const DEFAULT_PLAN_FILE = "chant.tfplan";
|
|
28
|
+
/** Binary used when the `terraform` namespace records no preference. */
|
|
29
|
+
export declare const DEFAULT_TERRAFORM_BINARY = "terraform";
|
|
30
|
+
/** Fields every activity here takes: which root, and where the project is. */
|
|
31
|
+
export interface TerraformRootArgs {
|
|
32
|
+
/**
|
|
33
|
+
* Key into `terraform.roots`. Not a directory — the directory, workspace,
|
|
34
|
+
* var files and backend config all come from the named entry, so an Op step
|
|
35
|
+
* cannot drift from what the project declared.
|
|
36
|
+
*/
|
|
37
|
+
root: string;
|
|
38
|
+
/**
|
|
39
|
+
* Directory to start the `chant.config.*` search from. Default:
|
|
40
|
+
* `process.cwd()`, which is the project root under `chant run`.
|
|
41
|
+
*/
|
|
42
|
+
cwd?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface TerraformInitArgs extends TerraformRootArgs {
|
|
45
|
+
/** `-upgrade`: re-resolve provider and module versions within constraints. */
|
|
46
|
+
upgrade?: boolean;
|
|
47
|
+
/** `-reconfigure`: ignore any existing backend state and configure afresh. */
|
|
48
|
+
reconfigure?: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface TerraformPlanArgs extends TerraformRootArgs {
|
|
51
|
+
/** Plan file to write, relative to the root directory. Default: {@link DEFAULT_PLAN_FILE}. */
|
|
52
|
+
planFile?: string;
|
|
53
|
+
/** `-destroy`: plan the removal of everything the root manages. */
|
|
54
|
+
destroy?: boolean;
|
|
55
|
+
}
|
|
56
|
+
export interface TerraformApplyArgs extends TerraformRootArgs {
|
|
57
|
+
/**
|
|
58
|
+
* The saved plan file to apply, relative to the root directory — normally
|
|
59
|
+
* `plan.out.planFile`, the Plan step's own output. Required: this activity
|
|
60
|
+
* has no bare-apply mode.
|
|
61
|
+
*/
|
|
62
|
+
planFile: string;
|
|
63
|
+
}
|
|
64
|
+
export interface TerraformShowArgs extends TerraformRootArgs {
|
|
65
|
+
/** Show this saved plan file. Omitted, the activity shows current state. */
|
|
66
|
+
planFile?: string;
|
|
67
|
+
}
|
|
68
|
+
/** What {@link terraformInit} resolved. */
|
|
69
|
+
export interface TerraformInitResult {
|
|
70
|
+
/** Absolute path of the root module directory that was initialized. */
|
|
71
|
+
dir: string;
|
|
72
|
+
/** Workspace the run selected via `TF_WORKSPACE`, when the root names one. */
|
|
73
|
+
workspace?: string;
|
|
74
|
+
}
|
|
75
|
+
/** Counts projected out of a plan's `resource_changes`. */
|
|
76
|
+
export interface PlanChangeCounts {
|
|
77
|
+
/** Resources to create. A replace counts here and in `destroys`, as terraform's own summary does. */
|
|
78
|
+
adds: number;
|
|
79
|
+
/** Resources to update in place. */
|
|
80
|
+
changes: number;
|
|
81
|
+
/** Resources to destroy. This is the number an approval gate exists for. */
|
|
82
|
+
destroys: number;
|
|
83
|
+
}
|
|
84
|
+
/** What {@link terraformPlan} resolved. Carries the plan itself, so no later step re-plans. */
|
|
85
|
+
export interface TerraformPlanResult extends PlanChangeCounts {
|
|
86
|
+
/** `true` when terraform reported exit 2 under `-detailed-exitcode`: the plan proposes changes. */
|
|
87
|
+
changed: boolean;
|
|
88
|
+
/** The written plan file, relative to `dir` — hand this straight to {@link terraformApply}. */
|
|
89
|
+
planFile: string;
|
|
90
|
+
/** Absolute path of the root module directory. */
|
|
91
|
+
dir: string;
|
|
92
|
+
/** `terraform show -json <planFile>`, parsed. */
|
|
93
|
+
json: unknown;
|
|
94
|
+
/** `terraform show -no-color <planFile>` — the human-readable plan. */
|
|
95
|
+
text: string;
|
|
96
|
+
}
|
|
97
|
+
/** What {@link terraformApply} resolved. */
|
|
98
|
+
export interface TerraformApplyResult {
|
|
99
|
+
/** The plan file that was applied. */
|
|
100
|
+
planFile: string;
|
|
101
|
+
/** Absolute path of the root module directory. */
|
|
102
|
+
dir: string;
|
|
103
|
+
/** Always `true` on success; the activity throws otherwise. */
|
|
104
|
+
applied: boolean;
|
|
105
|
+
}
|
|
106
|
+
/** What {@link terraformShow} resolved. */
|
|
107
|
+
export interface TerraformShowResult extends PlanChangeCounts {
|
|
108
|
+
/** Whether the output describes a saved plan or current state. */
|
|
109
|
+
source: "plan" | "state";
|
|
110
|
+
/** The `-json` output, parsed. */
|
|
111
|
+
json: unknown;
|
|
112
|
+
/** The `-no-color` output. */
|
|
113
|
+
text: string;
|
|
114
|
+
/** Absolute path of the root module directory. */
|
|
115
|
+
dir: string;
|
|
116
|
+
/** Present when `source` is `"plan"`. */
|
|
117
|
+
planFile?: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Quote a command-line argument for the shell `exec` runs it through, leaving
|
|
121
|
+
* ordinary paths and `key=value` pairs untouched so command strings stay
|
|
122
|
+
* readable in logs and in tests.
|
|
123
|
+
*/
|
|
124
|
+
export declare function quoteArg(value: string): string;
|
|
125
|
+
/** Which CLI drives the roots: `terraform.binary`, defaulting to `terraform`. */
|
|
126
|
+
export declare function terraformBinary(config?: TerraformConfig): string;
|
|
127
|
+
/**
|
|
128
|
+
* Environment for every invocation. `TF_IN_AUTOMATION` suppresses the
|
|
129
|
+
* "run terraform apply next" hand-holding and is terraform's own signal that
|
|
130
|
+
* no human is watching; `TF_WORKSPACE` is how a workspace gets selected
|
|
131
|
+
* without a separate `terraform workspace select` round-trip.
|
|
132
|
+
*/
|
|
133
|
+
export declare function terraformEnvironment(root?: Pick<TerraformRootConfig, "workspace">): Record<string, string>;
|
|
134
|
+
/** `terraform init`, with the root's `backendConfig` as `-backend-config=k=v` flags. */
|
|
135
|
+
export declare function terraformInitCommand(opts: {
|
|
136
|
+
binary: string;
|
|
137
|
+
backendConfig?: Record<string, string>;
|
|
138
|
+
upgrade?: boolean;
|
|
139
|
+
reconfigure?: boolean;
|
|
140
|
+
}): string;
|
|
141
|
+
/**
|
|
142
|
+
* `terraform plan`. `-detailed-exitcode` is what makes the run answerable:
|
|
143
|
+
* 0 means no changes, 2 means changes, and anything else is a failure. See
|
|
144
|
+
* {@link terraformPlan}.
|
|
145
|
+
*/
|
|
146
|
+
export declare function terraformPlanCommand(opts: {
|
|
147
|
+
binary: string;
|
|
148
|
+
planFile: string;
|
|
149
|
+
varFiles?: string[];
|
|
150
|
+
destroy?: boolean;
|
|
151
|
+
}): string;
|
|
152
|
+
/** `terraform apply <planFile>` — a saved plan, never a bare apply. */
|
|
153
|
+
export declare function terraformApplyCommand(opts: {
|
|
154
|
+
binary: string;
|
|
155
|
+
planFile: string;
|
|
156
|
+
}): string;
|
|
157
|
+
/**
|
|
158
|
+
* `terraform show`. The one command here that takes no `-input` flag
|
|
159
|
+
* (terraform answers `flag provided but not defined: -input`), so automation
|
|
160
|
+
* rests on `TF_IN_AUTOMATION` alone. Show reads an artifact and prompts for
|
|
161
|
+
* nothing in the first place.
|
|
162
|
+
*/
|
|
163
|
+
export declare function terraformShowCommand(opts: {
|
|
164
|
+
binary: string;
|
|
165
|
+
json: boolean;
|
|
166
|
+
planFile?: string;
|
|
167
|
+
}): string;
|
|
168
|
+
/**
|
|
169
|
+
* Project a plan's `resource_changes` into add/change/destroy counts, the same
|
|
170
|
+
* three terraform prints at the end of a plan. A replace (`["delete","create"]`)
|
|
171
|
+
* counts as one add and one destroy, exactly as terraform reports it.
|
|
172
|
+
*/
|
|
173
|
+
export declare function countPlanChanges(planJson: unknown): PlanChangeCounts;
|
|
174
|
+
/**
|
|
175
|
+
* `terraform init` in the named root, with the root's `backendConfig` supplied
|
|
176
|
+
* as `-backend-config` flags. Uses the longInfra profile: init downloads
|
|
177
|
+
* providers and modules, a network call of unpredictable size.
|
|
178
|
+
*/
|
|
179
|
+
export declare function terraformInit(args: TerraformInitArgs, signal?: AbortSignal): Promise<TerraformInitResult>;
|
|
180
|
+
/**
|
|
181
|
+
* `terraform plan -detailed-exitcode -out=<planFile>` in the named root, then
|
|
182
|
+
* `terraform show` over the written plan in both `-json` and `-no-color` form.
|
|
183
|
+
*
|
|
184
|
+
* The exit code is the answer, not an error condition: 0 is a plan with no
|
|
185
|
+
* changes, 2 is a plan with changes, and anything else, 1 included, is a
|
|
186
|
+
* failure thrown with terraform's own stderr attached. Both `show` renders
|
|
187
|
+
* come back with the result, so a gate, a report or an apply downstream reads
|
|
188
|
+
* the plan that ran instead of planning again against a moved world.
|
|
189
|
+
*
|
|
190
|
+
* Uses the longInfra profile: plan refreshes every resource against its
|
|
191
|
+
* provider.
|
|
192
|
+
*/
|
|
193
|
+
export declare function terraformPlan(args: TerraformPlanArgs, signal?: AbortSignal): Promise<TerraformPlanResult>;
|
|
194
|
+
/**
|
|
195
|
+
* `terraform apply <planFile>` in the named root. A saved plan only: without
|
|
196
|
+
* one, apply re-plans at apply time and acts on something no gate ever saw,
|
|
197
|
+
* so a missing `planFile` is refused here rather than quietly widened into a
|
|
198
|
+
* bare apply.
|
|
199
|
+
*
|
|
200
|
+
* Uses the longInfra profile.
|
|
201
|
+
*/
|
|
202
|
+
export declare function terraformApply(args: TerraformApplyArgs, signal?: AbortSignal): Promise<TerraformApplyResult>;
|
|
203
|
+
/**
|
|
204
|
+
* `terraform show` over current state, or over a saved plan when `planFile` is
|
|
205
|
+
* given. Returns the parsed `-json` output and the `-no-color` render, plus
|
|
206
|
+
* the add/change/destroy counts when the subject is a plan. State has no
|
|
207
|
+
* change set, so for state the three counts are zero.
|
|
208
|
+
*
|
|
209
|
+
* Uses the fastIdempotent profile: show reads an artifact and calls no
|
|
210
|
+
* provider.
|
|
211
|
+
*/
|
|
212
|
+
export declare function terraformShow(args: TerraformShowArgs, signal?: AbortSignal): Promise<TerraformShowResult>;
|
|
213
|
+
//# sourceMappingURL=terraform.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terraform.d.ts","sourceRoot":"","sources":["../../../src/op/activities/terraform.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed step-builder wrappers for this lexicon's activities, copied from
|
|
3
|
+
* `lexicons/k3s/src/op/builders.ts` (chant #1288 Stage 2). `opts`'s type in
|
|
4
|
+
* each wrapper below IS the activity's own `*Args` interface, via
|
|
5
|
+
* `Omit`/`WithStepRefs`, never restated: rename a field on the activity and
|
|
6
|
+
* the builder's callers stop compiling.
|
|
7
|
+
*
|
|
8
|
+
* The `root` key is positional in every wrapper, because naming the root is
|
|
9
|
+
* the one thing a terraform step cannot be authored without.
|
|
10
|
+
*
|
|
11
|
+
* `id` routes to the step's `id` field rather than into `args`. That matters
|
|
12
|
+
* here more than anywhere else in the lexicon: `.out` throws without an id
|
|
13
|
+
* (`packages/core/src/op/builders.ts`), and `.out` is how an Apply step names
|
|
14
|
+
* the Plan step's `planFile` as a `StepOutputRef` instead of guessing the
|
|
15
|
+
* path a second time.
|
|
16
|
+
*/
|
|
17
|
+
import { type ActivityStep, type NamedActivityStep, type WithStepRefs } from "@intentius/chant/op";
|
|
18
|
+
import type { TerraformInitArgs, TerraformPlanArgs, TerraformApplyArgs, TerraformShowArgs } from "./activities/terraform.js";
|
|
19
|
+
/** Extra opts every wrapper below accepts alongside its activity's own fields. */
|
|
20
|
+
type StepOpts = {
|
|
21
|
+
profile?: ActivityStep["profile"];
|
|
22
|
+
id?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* `terraform init` in the named root — the fully typed twin of the
|
|
26
|
+
* `terraformInit` activity. `opts` is {@link TerraformInitArgs} itself, minus
|
|
27
|
+
* the positional `root`. Defaults to the `longInfra` profile: init downloads
|
|
28
|
+
* providers.
|
|
29
|
+
*/
|
|
30
|
+
export declare const terraformInit: (root: string, opts?: WithStepRefs<Omit<TerraformInitArgs, "root">> & StepOpts) => NamedActivityStep;
|
|
31
|
+
/**
|
|
32
|
+
* `terraform plan` in the named root, writing a saved plan. `opts` is
|
|
33
|
+
* {@link TerraformPlanArgs} itself, minus the positional `root`. Defaults to
|
|
34
|
+
* the `longInfra` profile: plan refreshes every resource against its provider.
|
|
35
|
+
*
|
|
36
|
+
* Give this step an `id` when a later step applies its plan — `plan.out.planFile`
|
|
37
|
+
* is the reference an Apply step takes.
|
|
38
|
+
*/
|
|
39
|
+
export declare const terraformPlan: (root: string, opts?: WithStepRefs<Omit<TerraformPlanArgs, "root">> & StepOpts) => NamedActivityStep;
|
|
40
|
+
/**
|
|
41
|
+
* `terraform apply <planFile>` in the named root — the fully typed twin of the
|
|
42
|
+
* `terraformApply` activity. `opts` is {@link TerraformApplyArgs} itself,
|
|
43
|
+
* minus the positional `root`, so `planFile` stays required at the call site
|
|
44
|
+
* and the activity's bare-apply refusal is a compile error rather than a
|
|
45
|
+
* runtime one. Defaults to the `longInfra` profile.
|
|
46
|
+
*/
|
|
47
|
+
export declare const terraformApply: (root: string, opts: WithStepRefs<Omit<TerraformApplyArgs, "root">> & StepOpts) => NamedActivityStep;
|
|
48
|
+
/**
|
|
49
|
+
* `terraform show` in the named root, over state or over a saved plan. `opts`
|
|
50
|
+
* is {@link TerraformShowArgs} itself, minus the positional `root`. Defaults
|
|
51
|
+
* to the `fastIdempotent` profile: show reads an artifact and calls no
|
|
52
|
+
* provider.
|
|
53
|
+
*/
|
|
54
|
+
export declare const terraformShow: (root: string, opts?: WithStepRefs<Omit<TerraformShowArgs, "root">> & StepOpts) => NamedActivityStep;
|
|
55
|
+
export {};
|
|
56
|
+
//# sourceMappingURL=builders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../../src/op/builders.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EACV,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AAEhC,kFAAkF;AAClF,KAAK,QAAQ,GAAG;IAAE,OAAO,CAAC,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,MAAM,EACZ,OAAO,YAAY,CAAC,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,GAAG,QAAQ,KAC9D,iBAGF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,MAAM,EACZ,OAAO,YAAY,CAAC,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,GAAG,QAAQ,KAC9D,iBAGF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,MAAM,EACZ,MAAM,YAAY,CAAC,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC,GAAG,QAAQ,KAC9D,iBAGF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,MAAM,EACZ,OAAO,YAAY,CAAC,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,GAAG,QAAQ,KAC9D,iBAOF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-cli.d.ts","sourceRoot":"","sources":["../src/package-cli.ts"],"names":[],"mappings":""}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LexiconPlugin } from "@intentius/chant/lexicon";
|
|
2
|
+
/**
|
|
3
|
+
* terraform lexicon plugin.
|
|
4
|
+
*
|
|
5
|
+
* There is no upstream schema to pin here: Terraform's resource surface lives
|
|
6
|
+
* in provider registries, one schema per provider, and this lexicon reads the
|
|
7
|
+
* HCL an estate already has rather than generating types for it. So `spec/`
|
|
8
|
+
* and `codegen/` stay near-empty and there is no `upstreamPin`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const terraformPlugin: LexiconPlugin;
|
|
11
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAwB9D;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,EAAE,aA4H7B,CAAC"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TF101: terraformApply's planFile must reference a preceding terraformPlan step.
|
|
6
|
+
*
|
|
7
|
+
* Applying a plan file chosen by a string means the applied plan can be
|
|
8
|
+
* stale, hand-edited, or simply the wrong one — the entire point of the
|
|
9
|
+
* plan/apply split (review exactly what will change, then apply exactly
|
|
10
|
+
* that) is defeated the moment the path is spelled out in source instead of
|
|
11
|
+
* carried forward from the `terraformPlan` step that produced it.
|
|
12
|
+
*
|
|
13
|
+
* The two builders this rule is written against (`terraformPlan`,
|
|
14
|
+
* `terraformApply`) don't exist yet (#2086 ships them in parallel), so this
|
|
15
|
+
* matches purely on callee identifier name — same posture as
|
|
16
|
+
* `lexicons/k3s/src/lint/rules/token-literal.ts`.
|
|
17
|
+
*
|
|
18
|
+
* Accepts exactly the two authoring forms that produce a genuine step-output
|
|
19
|
+
* reference (`packages/core/src/op/step-output-ref.ts`):
|
|
20
|
+
* - `stepOutput(<ident>, "planFile")`
|
|
21
|
+
* - `<ident>.out.planFile`
|
|
22
|
+
* where `<ident>` is bound (`const <ident> = terraformPlan(...)`) in the same
|
|
23
|
+
* file. Flags a string literal, a template literal, or one of the two
|
|
24
|
+
* accepted reference forms pointing at an identifier that resolves to
|
|
25
|
+
* something other than a `terraformPlan` call. When `<ident>` cannot be
|
|
26
|
+
* resolved at all (not declared anywhere in the file), this rule stays
|
|
27
|
+
* silent — the same deliberately shallow "bail rather than guess" posture
|
|
28
|
+
* `validateStepOutputRefs` takes, since the runtime brand on a real
|
|
29
|
+
* `StepOutputRef` is invisible to a source-level AST match.
|
|
30
|
+
*/
|
|
31
|
+
export const planBeforeApplyRule: LintRule = {
|
|
32
|
+
id: "TF101",
|
|
33
|
+
severity: "error",
|
|
34
|
+
category: "correctness",
|
|
35
|
+
description: "terraformApply's planFile must reference a preceding terraformPlan step",
|
|
36
|
+
|
|
37
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
38
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
39
|
+
const source = context.sourceFile;
|
|
40
|
+
|
|
41
|
+
/** Identifier name bound to a `terraformPlan(...)` call, to whether it truly is one. */
|
|
42
|
+
const bindings = new Map<string, boolean>();
|
|
43
|
+
|
|
44
|
+
const calleeName = (expr: ts.Expression): string | undefined =>
|
|
45
|
+
ts.isIdentifier(expr) ? expr.text : ts.isPropertyAccessExpression(expr) ? expr.name.text : undefined;
|
|
46
|
+
|
|
47
|
+
const collectBindings = (node: ts.Node) => {
|
|
48
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
49
|
+
const init = node.initializer;
|
|
50
|
+
if (ts.isCallExpression(init) || ts.isNewExpression(init)) {
|
|
51
|
+
bindings.set(node.name.text, calleeName(init.expression) === "terraformPlan");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
ts.forEachChild(node, collectBindings);
|
|
55
|
+
};
|
|
56
|
+
collectBindings(source);
|
|
57
|
+
|
|
58
|
+
const flag = (node: ts.Node, message: string) => {
|
|
59
|
+
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
60
|
+
diagnostics.push({
|
|
61
|
+
ruleId: "TF101",
|
|
62
|
+
severity: "error",
|
|
63
|
+
message,
|
|
64
|
+
file: context.filePath,
|
|
65
|
+
line: line + 1,
|
|
66
|
+
column: character + 1,
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* `stepOutput(<ident>, "planFile")` or `<ident>.out.planFile`, returning
|
|
72
|
+
* the referenced identifier's name, or `undefined` if `initializer`
|
|
73
|
+
* isn't one of those two shapes.
|
|
74
|
+
*/
|
|
75
|
+
const referencedIdent = (initializer: ts.Expression): string | undefined => {
|
|
76
|
+
if (ts.isCallExpression(initializer) && calleeName(initializer.expression) === "stepOutput") {
|
|
77
|
+
const arg = initializer.arguments[0];
|
|
78
|
+
return arg && ts.isIdentifier(arg) ? arg.text : undefined;
|
|
79
|
+
}
|
|
80
|
+
if (
|
|
81
|
+
ts.isPropertyAccessExpression(initializer) &&
|
|
82
|
+
initializer.name.text === "planFile" &&
|
|
83
|
+
ts.isPropertyAccessExpression(initializer.expression) &&
|
|
84
|
+
initializer.expression.name.text === "out" &&
|
|
85
|
+
ts.isIdentifier(initializer.expression.expression)
|
|
86
|
+
) {
|
|
87
|
+
return initializer.expression.expression.text;
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const checkPlanFile = (obj: ts.ObjectLiteralExpression) => {
|
|
93
|
+
for (const prop of obj.properties) {
|
|
94
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
95
|
+
const name = ts.isIdentifier(prop.name)
|
|
96
|
+
? prop.name.text
|
|
97
|
+
: ts.isStringLiteral(prop.name)
|
|
98
|
+
? prop.name.text
|
|
99
|
+
: undefined;
|
|
100
|
+
if (name !== "planFile") continue;
|
|
101
|
+
|
|
102
|
+
const value = prop.initializer;
|
|
103
|
+
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value) || ts.isTemplateExpression(value)) {
|
|
104
|
+
flag(
|
|
105
|
+
value,
|
|
106
|
+
"`planFile` is a literal path in source. Pass the preceding `terraformPlan` step's output " +
|
|
107
|
+
'(`stepOutput(plan, "planFile")` or `plan.out.planFile`), not a hardcoded path.',
|
|
108
|
+
);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const ident = referencedIdent(value);
|
|
113
|
+
if (ident === undefined) continue; // not one of the two accepted forms — out of this rule's reach
|
|
114
|
+
const isPlan = bindings.get(ident);
|
|
115
|
+
if (isPlan === false) {
|
|
116
|
+
flag(
|
|
117
|
+
value,
|
|
118
|
+
`\`planFile\` references "${ident}", which is not bound to a \`terraformPlan(...)\` call. ` +
|
|
119
|
+
"Apply only a plan this Op itself produced.",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
// isPlan === true: paired correctly.
|
|
123
|
+
// isPlan === undefined: `ident` isn't declared anywhere in this file — cannot resolve
|
|
124
|
+
// statically, so stay silent rather than guess.
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const visit = (node: ts.Node) => {
|
|
129
|
+
if (ts.isCallExpression(node) && calleeName(node.expression) === "terraformApply") {
|
|
130
|
+
// The options object may be the first argument (`terraformApply({ planFile })`)
|
|
131
|
+
// or follow a positional one (`terraformApply("app", { planFile })`, the
|
|
132
|
+
// k3s-shaped builder signature); inspect every object-literal argument.
|
|
133
|
+
for (const arg of node.arguments) {
|
|
134
|
+
if (ts.isObjectLiteralExpression(arg)) checkPlanFile(arg);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
ts.forEachChild(node, visit);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
visit(source);
|
|
141
|
+
return diagnostics;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TF001: a root module keeps its state on local disk.
|
|
3
|
+
*
|
|
4
|
+
* A `terraform` block with no `backend "<type>"` and no `cloud {}` falls back
|
|
5
|
+
* to the local backend, which puts `terraform.tfstate` in the working
|
|
6
|
+
* directory. That file holds every attribute of every managed resource,
|
|
7
|
+
* secrets included, and it is not shared, not locked, and not versioned. The
|
|
8
|
+
* first apply from a second machine or a CI runner starts from an empty state
|
|
9
|
+
* and proposes to create the estate again.
|
|
10
|
+
*
|
|
11
|
+
* One diagnostic per root, fired from the root's `terraform` block. A root
|
|
12
|
+
* with no `terraform` block at all is not flagged: it declares no version
|
|
13
|
+
* constraints either, and the missing block is a different finding.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
PostSynthCheck,
|
|
18
|
+
PostSynthContext,
|
|
19
|
+
PostSynthDiagnostic,
|
|
20
|
+
} from "@intentius/chant/lint/post-synth";
|
|
21
|
+
import { isResourceDeclarable } from "@intentius/chant/declarable";
|
|
22
|
+
import { TERRAFORM_TYPE, type BlockBody } from "../../hcl/parse";
|
|
23
|
+
|
|
24
|
+
/** `backend`/`cloud` are blocks, so hcl2json encodes them as a value under the key. */
|
|
25
|
+
function hasBlock(body: BlockBody, key: string): boolean {
|
|
26
|
+
const value = body[key];
|
|
27
|
+
if (value === undefined || value === null) return false;
|
|
28
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
29
|
+
return typeof value === "object" ? Object.keys(value as object).length > 0 : true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const tf001: PostSynthCheck = {
|
|
33
|
+
id: "TF001",
|
|
34
|
+
description: "Root module declares no remote backend",
|
|
35
|
+
|
|
36
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
37
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
38
|
+
/** Root name to the entity key of the first `terraform` block seen for it. */
|
|
39
|
+
const flagged = new Set<string>();
|
|
40
|
+
|
|
41
|
+
for (const [name, entity] of ctx.entities) {
|
|
42
|
+
if (entity.entityType !== TERRAFORM_TYPE) continue;
|
|
43
|
+
if (!isResourceDeclarable(entity)) continue;
|
|
44
|
+
const props = entity.props as { root?: unknown; body?: unknown };
|
|
45
|
+
const root = typeof props.root === "string" ? props.root : "";
|
|
46
|
+
if (flagged.has(root)) continue;
|
|
47
|
+
|
|
48
|
+
const body = (typeof props.body === "object" && props.body !== null ? props.body : {}) as BlockBody;
|
|
49
|
+
if (hasBlock(body, "backend") || hasBlock(body, "cloud")) continue;
|
|
50
|
+
|
|
51
|
+
flagged.add(root);
|
|
52
|
+
diagnostics.push({
|
|
53
|
+
checkId: "TF001",
|
|
54
|
+
severity: "warning",
|
|
55
|
+
message:
|
|
56
|
+
`Root module "${root}" declares no remote backend. State falls back to a local ` +
|
|
57
|
+
"terraform.tfstate, which is unshared, unlocked and holds every resource attribute in " +
|
|
58
|
+
'plaintext. Add a `backend "<type>"` or a `cloud {}` block to the terraform block.',
|
|
59
|
+
entity: name,
|
|
60
|
+
lexicon: "terraform",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return diagnostics;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Serializer } from "@intentius/chant";
|
|
2
|
+
/**
|
|
3
|
+
* terraform serializer.
|
|
4
|
+
*
|
|
5
|
+
* A stub, deliberately. The entities this lexicon carries come from `.tf`
|
|
6
|
+
* files that already exist on disk (`buildRoots()` parses the roots named in
|
|
7
|
+
* `terraform.roots`), so there is nothing for chant to write back: emitting
|
|
8
|
+
* HCL here would put a second, generated copy of the estate beside the
|
|
9
|
+
* authored one. The lexicon's value is what the entities let the rest of
|
|
10
|
+
* chant do with the root (post-synth checks, `chant audit`, the Op surface),
|
|
11
|
+
* not a rendered artifact.
|
|
12
|
+
*
|
|
13
|
+
* `name` and `rulePrefix` are the two members the `Serializer` contract
|
|
14
|
+
* requires, and both are load-bearing: `name` is the key `build()` files this
|
|
15
|
+
* lexicon's output under, and `rulePrefix` is what every `TF*` id is checked
|
|
16
|
+
* against.
|
|
17
|
+
*/
|
|
18
|
+
export declare const terraformSerializer: Serializer;
|
|
19
|
+
//# sourceMappingURL=serializer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE/D;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,mBAAmB,EAAE,UAOjC,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
skill: chant-terraform
|
|
3
|
+
description: Read an existing Terraform root module into chant's build and audit, and drive it with the init/plan/apply Ops
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Terraform as an Existing Estate
|
|
8
|
+
|
|
9
|
+
## What this lexicon covers
|
|
10
|
+
|
|
11
|
+
There is no generated resource surface here, and nothing is written back over the HCL. Terraform's own `.tf` files stay the only source of truth, and `terraform apply` keeps working exactly as before. What chant adds is: one entity per HCL block, read at build time, so the post-synth checks and `chant audit` have something to see; and Op activities that drive `init`/`plan`/`apply`/`show` against a saved plan.
|
|
12
|
+
|
|
13
|
+
## Naming your roots
|
|
14
|
+
|
|
15
|
+
Add the `terraform` namespace to `chant.config.ts`. Importing the package brings the key into `ChantConfig`.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import type { ChantConfig } from "@intentius/chant/config";
|
|
19
|
+
import "@intentius/chant-lexicon-terraform";
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
lexicons: ["terraform"],
|
|
23
|
+
terraform: {
|
|
24
|
+
binary: "terraform", // or "tofu" — the two are wire-compatible for everything this lexicon does
|
|
25
|
+
roots: {
|
|
26
|
+
app: { dir: "./terraform/app", workspace: "prod", varFiles: ["prod.tfvars"] },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
} satisfies ChantConfig;
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`dir` is the only required field, resolved against the project root (where `chant.config.ts` lives), not against the cwd a step happens to run from. The root's name (`app` above) is the entity-key prefix and the string every Op step and builder takes as `root` — keep it stable once other declarations reference it.
|
|
33
|
+
|
|
34
|
+
## What a build produces
|
|
35
|
+
|
|
36
|
+
Every block of every configured root becomes one entity, keyed `<root>/<address>`: `terraform { }` -> `Terraform::Terraform`, `provider "x" {}` -> `Terraform::Provider`, `resource "x" "y" {}` -> `Terraform::Resource`, `data "x" "y" {}` -> `Terraform::Data`, `module "x" {}` -> `Terraform::Module`, `variable "x" {}` -> `Terraform::Variable`, `output "x" {}` -> `Terraform::Output`, `locals {}` -> `Terraform::Locals`. Each carries `props.address`, `props.body` (the block, verbatim), `props.file` and `props.root`. A root whose `dir` doesn't exist, or whose HCL the parser refuses, warns and contributes no entities — the rest of the build is unaffected.
|
|
37
|
+
|
|
38
|
+
## The one thing to check before anything else
|
|
39
|
+
|
|
40
|
+
`TF001` fires once per root whose `terraform` block declares neither a `backend "<type>"` nor a `cloud {}`. That root's state is a local `terraform.tfstate`: unshared, unlocked, holding every resource attribute in plaintext. Add a real backend before anything else touches this root:
|
|
41
|
+
|
|
42
|
+
```hcl
|
|
43
|
+
terraform {
|
|
44
|
+
backend "s3" {
|
|
45
|
+
bucket = "acme-tfstate"
|
|
46
|
+
key = "app/terraform.tfstate"
|
|
47
|
+
region = "us-east-1"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`TF101` is the second rule, and it's about the Ops below: a `terraformApply` step's `planFile` must reference the output of a preceding `terraformPlan` step (`plan.out.planFile`), never a literal path. Applying an unreviewed plan, or a plan somebody hand-edited on disk, is exactly what a saved-plan discipline exists to prevent.
|
|
53
|
+
|
|
54
|
+
## Driving a root with an Op
|
|
55
|
+
|
|
56
|
+
`TerraformApplyOp` is Init, Plan, an optional approval Gate, then Apply — Init and Plan on the `longInfra` profile, the Gate's `show` step on `fastIdempotent`:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { TerraformApplyOp } from "@intentius/chant-lexicon-terraform";
|
|
60
|
+
|
|
61
|
+
export const { op } = TerraformApplyOp({
|
|
62
|
+
name: "app-apply",
|
|
63
|
+
root: "app",
|
|
64
|
+
gate: "on-destroy", // default — "always" gates every apply, "never" drops the Gate phase entirely
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`gate: "never"` is what makes the Op runnable under the local executor (`chant run`); any other mode emits a Gate phase, and a gate is durable-runtime work the local executor refuses. The Gate phase always shows the saved plan first and reports its `destroys` count as a `Destroys` search attribute, because `GateStep` carries no condition to branch on at build time — the approver sees what's at stake before approving, rather than the Op deciding for them.
|
|
69
|
+
|
|
70
|
+
Terraform has no automatic rollback, so `compensate: true` with no command throws at build time, naming the Op, rather than warning once an apply has already half-run:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
export const { op } = TerraformApplyOp({
|
|
74
|
+
name: "app-apply-gated",
|
|
75
|
+
root: "app",
|
|
76
|
+
gate: "always",
|
|
77
|
+
compensate: { command: "terraform destroy -auto-approve" },
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Reach for the four builders directly (`terraformInit`, `terraformPlan`, `terraformApply`, `terraformShow`) when a phase shape other than Init/Plan/Gate/Apply is needed — `plan.out.planFile` is how a later step references an earlier Plan step's saved output, and it only resolves when the Plan step carries an `id`.
|
|
82
|
+
|
|
83
|
+
## Where a running estate's ownership answer lives
|
|
84
|
+
|
|
85
|
+
Terraform's own state file is the ownership answer for what it manages — this lexicon doesn't add a second one. See the lexicon's "Live Observation" doc page for how a scheduled watch reads that state.
|
|
86
|
+
|
|
87
|
+
## Rules
|
|
88
|
+
|
|
89
|
+
| Rule | Severity | What it catches |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| TF001 | error (post-synth) | root module declares no remote backend |
|
|
92
|
+
| TF101 | error | `terraformApply`'s `planFile` isn't a preceding `terraformPlan` step's output |
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
import type { ChannelKeys } from "@intentius/chant/ownership";
|
|
18
|
+
/**
|
|
19
|
+
* Where the verdict is read from, rather than what is written.
|
|
20
|
+
*
|
|
21
|
+
* {@link ChannelKeys} names tag or label keys everywhere else, because
|
|
22
|
+
* everywhere else the marker is a key/value pair on the resource. These three
|
|
23
|
+
* name the state file, the configured root that owns that state, and the
|
|
24
|
+
* workspace the root selects.
|
|
25
|
+
*/
|
|
26
|
+
export declare const TERRAFORM_STATE_OWNERSHIP_KEYS: ChannelKeys;
|
|
27
|
+
//# sourceMappingURL=state-ownership.d.ts.map
|