@intentius/chant-lexicon-terraform 0.58.0 → 0.59.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 +9 -0
- package/dist/composites/terraform-adopt-op.d.ts +7 -2
- package/dist/composites/terraform-adopt-op.d.ts.map +1 -1
- package/dist/composites/terraform-apply-op.d.ts +8 -2
- package/dist/composites/terraform-apply-op.d.ts.map +1 -1
- package/dist/composites/terraform-watch-op.d.ts +30 -7
- package/dist/composites/terraform-watch-op.d.ts.map +1 -1
- package/dist/hcl/parse.d.ts +14 -0
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/integrity.json +7 -5
- package/dist/lint/audit-catalog.d.ts +2 -2
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +39 -12
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/tf001.d.ts +40 -6
- package/dist/lint/post-synth/tf001.d.ts.map +1 -1
- package/dist/lint/post-synth/tf015.d.ts +11 -2
- package/dist/lint/post-synth/tf015.d.ts.map +1 -1
- package/dist/lint/post-synth/tf026.d.ts +7 -0
- package/dist/lint/post-synth/tf026.d.ts.map +1 -1
- package/dist/lint/post-synth/tf027.d.ts +30 -0
- package/dist/lint/post-synth/tf027.d.ts.map +1 -0
- package/dist/lint/post-synth/tf028.d.ts +33 -0
- package/dist/lint/post-synth/tf028.d.ts.map +1 -0
- package/dist/lsp/option-keys.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +2 -0
- package/dist/okf/rules/TF027.md +11 -0
- package/dist/okf/rules/TF028.md +11 -0
- package/dist/op/activities/terraform.d.ts +32 -11
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/builders.d.ts +7 -5
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/resolve-root-mode.d.ts +29 -15
- package/dist/op/resolve-root-mode.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/tf001.ts +70 -14
- package/dist/rules/tf015.ts +11 -2
- package/dist/rules/tf026.ts +6 -2
- package/dist/rules/tf027.ts +74 -0
- package/dist/rules/tf028.ts +96 -0
- package/dist/validate.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__fixtures__/ACCEPTANCE.md +181 -0
- package/src/__fixtures__/remote-backend/main.tf +34 -0
- package/src/__fixtures__/with-backend/main.tf +7 -0
- package/src/codegen/lint-rules-page.test.ts +18 -1
- package/src/composites/terraform-adopt-op.acceptance.test.ts +46 -19
- package/src/composites/terraform-adopt-op.test.ts +9 -4
- package/src/composites/terraform-adopt-op.ts +8 -3
- package/src/composites/terraform-apply-op.acceptance.test.ts +14 -9
- package/src/composites/terraform-apply-op.test.ts +13 -5
- package/src/composites/terraform-apply-op.ts +23 -7
- package/src/composites/terraform-watch-op.test.ts +85 -4
- package/src/composites/terraform-watch-op.ts +59 -8
- package/src/hcl/parse.ts +20 -0
- package/src/lint/audit-catalog.ts +26 -2
- package/src/lint/audit-lineage.ts +79 -14
- package/src/lint/audit.test.ts +171 -3
- package/src/lint/post-synth/fixtures/TF001/negative-cloud.tf +28 -0
- package/src/lint/post-synth/fixtures/TF001/negative.tf +4 -2
- package/src/lint/post-synth/fixtures/TF001/positive-local.tf +31 -0
- package/src/lint/post-synth/fixtures/TF014/negative/main.tf +2 -2
- package/src/lint/post-synth/fixtures/TF014/positive/main.tf +2 -2
- package/src/lint/post-synth/fixtures/TF020/scoped/main.tf +2 -2
- package/src/lint/post-synth/fixtures/TF027/live-default/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF027/live-untagged-delete/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF027/live-untagged-delete-scoped/main.tf +17 -0
- package/src/lint/post-synth/fixtures/TF027/live-untagged-report/main.tf +13 -0
- package/src/lint/post-synth/fixtures/TF028/live/main.tf +9 -0
- package/src/lint/post-synth/fixtures/TF028/stock/main.tf +10 -0
- package/src/lint/post-synth/index.ts +4 -0
- package/src/lint/post-synth/live-refusals-config-format.test.ts +143 -0
- package/src/lint/post-synth/module-rules.test.ts +1 -1
- package/src/lint/post-synth/post-synth.test.ts +64 -6
- package/src/lint/post-synth/tf001.ts +70 -14
- package/src/lint/post-synth/tf015.ts +11 -2
- package/src/lint/post-synth/tf026.ts +6 -2
- package/src/lint/post-synth/tf027.test.ts +62 -0
- package/src/lint/post-synth/tf027.ts +74 -0
- package/src/lint/post-synth/tf028.test.ts +92 -0
- package/src/lint/post-synth/tf028.ts +96 -0
- package/src/lsp/option-keys.ts +3 -2
- package/src/op/activities/choudoufu.acceptance.test.ts +7 -23
- package/src/op/activities/choudoufu.test.ts +49 -17
- package/src/op/activities/terraform.ts +67 -29
- package/src/op/builders.ts +7 -5
- package/src/op/resolve-root-mode.ts +29 -15
- package/src/plugin.test.ts +38 -0
- package/src/plugin.ts +122 -31
- package/src/validate.ts +10 -1
|
@@ -164,6 +164,18 @@ describe("choudoufuLivePlanCommand (#2103)", () => {
|
|
|
164
164
|
);
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
+
// choudoufu refuses `-estate` beside a `live` block or an `estate.chdf.hcl`
|
|
168
|
+
// sidecar, and since v0.14.0 it does not need one there (choudoufu #894, PR
|
|
169
|
+
// 915). Omitting the option is how a declared root is run.
|
|
170
|
+
test("no estate emits no -estate, for a root that names its own (choudoufu #894)", () => {
|
|
171
|
+
expect(choudoufuLivePlanCommand({ binary: "choudoufu", json: true })).toBe(
|
|
172
|
+
"choudoufu live-plan -detailed-exitcode -json",
|
|
173
|
+
);
|
|
174
|
+
expect(choudoufuLivePlanCommand({ binary: "choudoufu", json: false, noColor: true })).toBe(
|
|
175
|
+
"choudoufu live-plan -detailed-exitcode -no-color",
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
167
179
|
test("-adoption-only and -no-color ride the human form (#2105)", () => {
|
|
168
180
|
expect(
|
|
169
181
|
choudoufuLivePlanCommand({ binary: "choudoufu", estate: "prod", json: false, adoptionOnly: true, noColor: true }),
|
|
@@ -265,10 +277,10 @@ describe("isOlderVersion / parseChoudoufuVersion (#2103)", () => {
|
|
|
265
277
|
// ── The version check, wired into every activity via resolveRoot ───────────
|
|
266
278
|
|
|
267
279
|
describe("choudoufu version check (#2103)", () => {
|
|
268
|
-
test("refuses a binary older than the floor, which the
|
|
280
|
+
test("refuses a binary older than the floor, which the -json document moved to v0.14.0", async () => {
|
|
269
281
|
const dir = liveProject();
|
|
270
|
-
replies.push({ match: "version", reply: { stdout: "choudoufu v0.
|
|
271
|
-
await expect(choudoufuLiveCheck({ root: "estate", cwd: dir })).rejects.toThrow(/older than.*v0\.
|
|
282
|
+
replies.push({ match: "version", reply: { stdout: "choudoufu v0.13.0 (based on OpenTofu v1.13.0)\n", stderr: "" } });
|
|
283
|
+
await expect(choudoufuLiveCheck({ root: "estate", cwd: dir })).rejects.toThrow(/older than.*v0\.14\.0/);
|
|
272
284
|
});
|
|
273
285
|
|
|
274
286
|
test("passes at exactly the minimum version", async () => {
|
|
@@ -398,7 +410,7 @@ describe("choudoufuLivePlan (#2103)", () => {
|
|
|
398
410
|
test("exit 0: no drift, the document is captured and written, the human plan comes from a second call", async () => {
|
|
399
411
|
const dir = liveProject();
|
|
400
412
|
replies.push({ match: "live-plan -detailed-exitcode -json", reply: { stdout: DOC, stderr: "" } });
|
|
401
|
-
replies.push({ match: "live-plan -detailed-exitcode -no-color
|
|
413
|
+
replies.push({ match: "live-plan -detailed-exitcode -no-color", reply: { stdout: "No changes.\n", stderr: "" } });
|
|
402
414
|
|
|
403
415
|
const result = await choudoufuLivePlan({ root: "estate", cwd: dir });
|
|
404
416
|
expect(result.drift).toBe(false);
|
|
@@ -415,9 +427,12 @@ describe("choudoufuLivePlan (#2103)", () => {
|
|
|
415
427
|
const written = readFileSync(join(result.dir, DEFAULT_LIVE_PLAN_DOCUMENT_FILE), "utf-8");
|
|
416
428
|
expect(JSON.parse(written)).toEqual(JSON.parse(DOC));
|
|
417
429
|
|
|
430
|
+
// The root names its own estate, so neither call carries `-estate`:
|
|
431
|
+
// choudoufu refuses the flag beside a declaration, and since v0.14.0 it
|
|
432
|
+
// reads the declared name itself (choudoufu #894, PR 915).
|
|
418
433
|
expect(commandsRun()).toEqual([
|
|
419
|
-
"choudoufu live-plan -detailed-exitcode -json
|
|
420
|
-
"choudoufu live-plan -detailed-exitcode -no-color
|
|
434
|
+
"choudoufu live-plan -detailed-exitcode -json",
|
|
435
|
+
"choudoufu live-plan -detailed-exitcode -no-color",
|
|
421
436
|
]);
|
|
422
437
|
});
|
|
423
438
|
|
|
@@ -425,7 +440,7 @@ describe("choudoufuLivePlan (#2103)", () => {
|
|
|
425
440
|
const dir = liveProject();
|
|
426
441
|
replies.push({ match: "live-plan -detailed-exitcode -json", reply: execError(2, "", DOC) });
|
|
427
442
|
replies.push({
|
|
428
|
-
match: "live-plan -detailed-exitcode -no-color
|
|
443
|
+
match: "live-plan -detailed-exitcode -no-color",
|
|
429
444
|
reply: { stdout: "Plan: 2 to add, 0 to change, 1 to destroy.\n", stderr: "" },
|
|
430
445
|
});
|
|
431
446
|
|
|
@@ -448,14 +463,25 @@ describe("choudoufuLivePlan (#2103)", () => {
|
|
|
448
463
|
);
|
|
449
464
|
});
|
|
450
465
|
|
|
451
|
-
test("
|
|
452
|
-
const dir = liveProject();
|
|
466
|
+
test("a root declaring no estate carries the caller's as -estate", async () => {
|
|
467
|
+
const dir = liveProject({ withLiveBlock: false });
|
|
453
468
|
replies.push({ match: "live-plan -detailed-exitcode -json", reply: { stdout: DOC, stderr: "" } });
|
|
454
|
-
replies.push({ match: "live-plan -detailed-exitcode -no-color
|
|
469
|
+
replies.push({ match: "live-plan -detailed-exitcode -no-color", reply: { stdout: "", stderr: "" } });
|
|
455
470
|
await choudoufuLivePlan({ root: "estate", cwd: dir, estate: "other-estate" });
|
|
456
471
|
expect(commandsRun()[0]).toContain("-estate=other-estate");
|
|
457
472
|
});
|
|
458
473
|
|
|
474
|
+
// The declaration wins on the wire whatever the caller passes, because
|
|
475
|
+
// choudoufu refuses `-estate` beside one. The result still reports the
|
|
476
|
+
// estate the document names.
|
|
477
|
+
test("a root that declares its own estate emits no -estate, even when one is passed", async () => {
|
|
478
|
+
const dir = liveProject();
|
|
479
|
+
replies.push({ match: "live-plan -detailed-exitcode -json", reply: { stdout: DOC, stderr: "" } });
|
|
480
|
+
replies.push({ match: "live-plan -detailed-exitcode -no-color", reply: { stdout: "", stderr: "" } });
|
|
481
|
+
await choudoufuLivePlan({ root: "estate", cwd: dir, estate: "other-estate" });
|
|
482
|
+
expect(commandsRun().every((c) => !c.includes("-estate"))).toBe(true);
|
|
483
|
+
});
|
|
484
|
+
|
|
459
485
|
test("no estate anywhere: refused, and the plan itself never runs", async () => {
|
|
460
486
|
const dir = liveProject({ withLiveBlock: false });
|
|
461
487
|
await expect(choudoufuLivePlan({ root: "estate", cwd: dir })).rejects.toThrow(/no estate to run against/);
|
|
@@ -494,7 +520,7 @@ describe("choudoufuLivePlan adoption ledger (#2105)", () => {
|
|
|
494
520
|
const dir = liveProject();
|
|
495
521
|
replies.push({ match: "live-plan -detailed-exitcode -json", reply: { stdout: ADOPTABLE, stderr: "" } });
|
|
496
522
|
replies.push({
|
|
497
|
-
match: "live-plan -detailed-exitcode -no-color
|
|
523
|
+
match: "live-plan -detailed-exitcode -no-color",
|
|
498
524
|
reply: { stdout: "Plan: 1 to add.\n", stderr: "" },
|
|
499
525
|
});
|
|
500
526
|
|
|
@@ -511,7 +537,7 @@ describe("choudoufuLivePlan adoption ledger (#2105)", () => {
|
|
|
511
537
|
const dir = liveProject();
|
|
512
538
|
replies.push({ match: "live-plan -detailed-exitcode -json", reply: { stdout: ADOPTABLE, stderr: "" } });
|
|
513
539
|
replies.push({
|
|
514
|
-
match: "live-plan -detailed-exitcode -no-color
|
|
540
|
+
match: "live-plan -detailed-exitcode -no-color",
|
|
515
541
|
reply: { stdout: "Plan: 1 to add.\n", stderr: "" },
|
|
516
542
|
});
|
|
517
543
|
|
|
@@ -531,8 +557,8 @@ describe("choudoufuLivePlan adoption ledger (#2105)", () => {
|
|
|
531
557
|
|
|
532
558
|
const result = await choudoufuLivePlan({ root: "estate", cwd: dir, adoptionOnly: true });
|
|
533
559
|
expect(commandsRun()).toEqual([
|
|
534
|
-
"choudoufu live-plan -detailed-exitcode -json
|
|
535
|
-
"choudoufu live-plan -detailed-exitcode -adoption-only -no-color
|
|
560
|
+
"choudoufu live-plan -detailed-exitcode -json",
|
|
561
|
+
"choudoufu live-plan -detailed-exitcode -adoption-only -no-color",
|
|
536
562
|
]);
|
|
537
563
|
expect(result.adoptions[0].command).toBe(
|
|
538
564
|
"aws ec2 create-tags --resources 'vpc-0abc' --tags 'Key=tofu-estate,Value=prod-networking'",
|
|
@@ -542,7 +568,7 @@ describe("choudoufuLivePlan adoption ledger (#2105)", () => {
|
|
|
542
568
|
test("without adoptionOnly a candidate carries the marker values and no command", async () => {
|
|
543
569
|
const dir = liveProject();
|
|
544
570
|
replies.push({ match: "live-plan -detailed-exitcode -json", reply: { stdout: ADOPTABLE, stderr: "" } });
|
|
545
|
-
replies.push({ match: "live-plan -detailed-exitcode -no-color
|
|
571
|
+
replies.push({ match: "live-plan -detailed-exitcode -no-color", reply: { stdout: "", stderr: "" } });
|
|
546
572
|
|
|
547
573
|
const result = await choudoufuLivePlan({ root: "estate", cwd: dir });
|
|
548
574
|
expect(result.adoptions[0].command).toBeUndefined();
|
|
@@ -676,8 +702,14 @@ describe("choudoufuLiveCheck (#2103)", () => {
|
|
|
676
702
|
});
|
|
677
703
|
});
|
|
678
704
|
|
|
679
|
-
|
|
680
|
-
|
|
705
|
+
// Defensive, and no longer describing a live defect: choudoufu v0.14.0 keeps
|
|
706
|
+
// `-json` stdout to the document, so the slice is the identity on every
|
|
707
|
+
// supported binary. These cases pin the behaviour that makes keeping it cheap:
|
|
708
|
+
// a stray writer ahead of the document costs a slice rather than a parse
|
|
709
|
+
// failure, and a stdout with no document at all still reaches `JSON.parse`
|
|
710
|
+
// with its real text.
|
|
711
|
+
describe("liveDocumentFrom (defensive, choudoufu #894 fixed in v0.14.0)", () => {
|
|
712
|
+
test("skips any line that precedes the document, if a writer ever puts one there", async () => {
|
|
681
713
|
const { liveDocumentFrom } = await import("./terraform");
|
|
682
714
|
const stdout = 'aws_vpc.main: Refreshing state... [id=vpc-1]\naws_subnet.a: Refreshing state...\n{\n "bound": []\n}\n';
|
|
683
715
|
expect(JSON.parse(liveDocumentFrom(stdout))).toEqual({ bound: [] });
|
|
@@ -77,10 +77,15 @@ export const DEFAULT_LIVE_PLAN_DOCUMENT_FILE = "chant.live-plan.json";
|
|
|
77
77
|
* ([choudoufu #878](https://github.com/INTENTIUS/choudoufu/issues/878), PR
|
|
78
78
|
* 889): under a live block `plan -out=FILE` is accepted and `apply FILE`
|
|
79
79
|
* re-plans the live system and refuses on a mismatch with exit status 3.
|
|
80
|
-
*
|
|
81
|
-
*
|
|
80
|
+
* v0.14.0 brought the `-json` document on a configuration that names its own
|
|
81
|
+
* estate ([choudoufu #894](https://github.com/INTENTIUS/choudoufu/issues/894),
|
|
82
|
+
* PR 915), which is the shape every chant live root has: before it,
|
|
83
|
+
* {@link choudoufuLivePlan} could not produce the document for a real root at
|
|
84
|
+
* all. {@link terraformApply} depends on the exit-3 refusal and
|
|
85
|
+
* `describeResources()` and both document-reading Ops depend on the document,
|
|
86
|
+
* so the floor is the release that shipped the later of the two.
|
|
82
87
|
*/
|
|
83
|
-
export const MIN_CHOUDOUFU_VERSION = "0.
|
|
88
|
+
export const MIN_CHOUDOUFU_VERSION = "0.14.0";
|
|
84
89
|
|
|
85
90
|
/**
|
|
86
91
|
* choudoufu's own refusal summary for `-out` on the `live-plan -estate`
|
|
@@ -480,10 +485,18 @@ export function terraformApplyCommand(opts: { binary: string; planFile: string }
|
|
|
480
485
|
}
|
|
481
486
|
|
|
482
487
|
/**
|
|
483
|
-
* `live-plan -detailed-exitcode -json -estate=<estate
|
|
488
|
+
* `live-plan -detailed-exitcode [-json] [-estate=<estate>]`. `-json` is what
|
|
484
489
|
* prints GitHub issue #788's document instead of the plan; the human render
|
|
485
490
|
* needs a second invocation without it (see {@link choudoufuLivePlan}).
|
|
486
491
|
*
|
|
492
|
+
* `estate` is omitted exactly when the configuration names its own estate.
|
|
493
|
+
* choudoufu refuses `-estate` beside a `live` block or an `estate.chdf.hcl`
|
|
494
|
+
* sidecar ("Estate named by both the live block and -estate"), and since
|
|
495
|
+
* v0.14.0 it does not need one: the run reads the declared name itself and
|
|
496
|
+
* prints the same document ([choudoufu #894](https://github.com/INTENTIUS/choudoufu/issues/894),
|
|
497
|
+
* PR 915). The flag is still how a root that declares nothing names an
|
|
498
|
+
* estate for the run.
|
|
499
|
+
*
|
|
487
500
|
* `adoptionOnly` makes that second invocation print the adoption ledger and
|
|
488
501
|
* nothing else (GitHub issue #587). It is refused here alongside `json`
|
|
489
502
|
* because choudoufu refuses the pair itself, and a builder that emitted a
|
|
@@ -492,7 +505,7 @@ export function terraformApplyCommand(opts: { binary: string; planFile: string }
|
|
|
492
505
|
*/
|
|
493
506
|
export function choudoufuLivePlanCommand(opts: {
|
|
494
507
|
binary: string;
|
|
495
|
-
estate
|
|
508
|
+
estate?: string;
|
|
496
509
|
json: boolean;
|
|
497
510
|
adoptionOnly?: boolean;
|
|
498
511
|
noColor?: boolean;
|
|
@@ -508,7 +521,7 @@ export function choudoufuLivePlanCommand(opts: {
|
|
|
508
521
|
if (opts.json) parts.push("-json");
|
|
509
522
|
if (opts.adoptionOnly) parts.push("-adoption-only");
|
|
510
523
|
if (opts.noColor) parts.push("-no-color");
|
|
511
|
-
parts.push(`-estate=${quoteArg(opts.estate)}`);
|
|
524
|
+
if (opts.estate !== undefined) parts.push(`-estate=${quoteArg(opts.estate)}`);
|
|
512
525
|
return parts.join(" ");
|
|
513
526
|
}
|
|
514
527
|
|
|
@@ -618,7 +631,7 @@ export function isOlderVersion(version: string, min: string): boolean {
|
|
|
618
631
|
|
|
619
632
|
/**
|
|
620
633
|
* Extract the release version from `choudoufu version`'s human-readable
|
|
621
|
-
* output: `"choudoufu v0.
|
|
634
|
+
* output: `"choudoufu v0.13.0 (based on OpenTofu v1.13.0)\non darwin_arm64"`.
|
|
622
635
|
* `choudoufu version -json`'s `terraform_version` field is the wrong number
|
|
623
636
|
* here: it names the upstream OpenTofu base this build forked from, not the
|
|
624
637
|
* choudoufu release itself (`internal/command/views/version.go`), so this
|
|
@@ -961,11 +974,19 @@ function resolveEstate(args: { estate?: string }, resolved: ResolvedRoot, activi
|
|
|
961
974
|
*/
|
|
962
975
|
/**
|
|
963
976
|
* The JSON document within a `live-plan -json` stdout: everything from the
|
|
964
|
-
* first line that is a bare `{` to the end.
|
|
965
|
-
*
|
|
966
|
-
*
|
|
967
|
-
*
|
|
968
|
-
*
|
|
977
|
+
* first line that is a bare `{` to the end. Returns the input unchanged when
|
|
978
|
+
* no such line exists.
|
|
979
|
+
*
|
|
980
|
+
* Defensive, and kept deliberately. choudoufu v0.14.0 keeps `-json` stdout to
|
|
981
|
+
* the document on every form of live root, so on a supported binary this
|
|
982
|
+
* returns its input unchanged and the slice never fires: it was written for
|
|
983
|
+
* the `-estate` path's refresh progress lines
|
|
984
|
+
* ([choudoufu #894](https://github.com/INTENTIUS/choudoufu/issues/894), fixed
|
|
985
|
+
* in PR 915, which routes every human-prose renderer of a `-json` run through
|
|
986
|
+
* `views.View.StdoutOnStderr()`). What it buys is that a future writer put
|
|
987
|
+
* back on stdout ahead of the document costs a stale slice rather than a
|
|
988
|
+
* `JSON.parse` failure, and a stdout carrying no document at all still fails
|
|
989
|
+
* in `JSON.parse` with the real text in the error.
|
|
969
990
|
*/
|
|
970
991
|
export function liveDocumentFrom(stdout: string): string {
|
|
971
992
|
const lines = stdout.split("\n");
|
|
@@ -982,10 +1003,14 @@ export async function choudoufuLivePlan(
|
|
|
982
1003
|
const estate = resolveEstate(args, resolved, "choudoufuLivePlan");
|
|
983
1004
|
const env = terraformEnvironment(resolved.root);
|
|
984
1005
|
const documentPath = args.documentPath ?? DEFAULT_LIVE_PLAN_DOCUMENT_FILE;
|
|
1006
|
+
// A root that declares its own estate is run without `-estate`: choudoufu
|
|
1007
|
+
// refuses the flag beside a declaration and, since v0.14.0, settles the name
|
|
1008
|
+
// from the declaration itself. See {@link choudoufuLivePlanCommand}.
|
|
1009
|
+
const estateFlag: { estate?: string } = resolved.estate === undefined ? { estate } : {};
|
|
985
1010
|
|
|
986
1011
|
let drift: boolean;
|
|
987
1012
|
let jsonStdout: string;
|
|
988
|
-
const planCmd = choudoufuLivePlanCommand({ binary,
|
|
1013
|
+
const planCmd = choudoufuLivePlanCommand({ binary, ...estateFlag, json: true });
|
|
989
1014
|
try {
|
|
990
1015
|
const { stdout, stderr } = await run(planCmd, dir, env, signal);
|
|
991
1016
|
report(stdout, stderr);
|
|
@@ -1005,8 +1030,8 @@ export async function choudoufuLivePlan(
|
|
|
1005
1030
|
jsonStdout = failure.stdout ?? "";
|
|
1006
1031
|
}
|
|
1007
1032
|
|
|
1008
|
-
//
|
|
1009
|
-
// the
|
|
1033
|
+
// Belt and braces: v0.14.0 keeps `-json` stdout to the document, so this is
|
|
1034
|
+
// the identity on a supported binary. See {@link liveDocumentFrom}.
|
|
1010
1035
|
const document = liveDocumentFrom(jsonStdout);
|
|
1011
1036
|
const json: unknown = JSON.parse(document);
|
|
1012
1037
|
// #788's document carries no render of the human plan itself
|
|
@@ -1015,19 +1040,32 @@ export async function choudoufuLivePlan(
|
|
|
1015
1040
|
// (#2105) changes which report that second read prints, never whether there
|
|
1016
1041
|
// is one: choudoufu refuses the flag alongside `-json`, so the two reports
|
|
1017
1042
|
// were always going to be two runs.
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1043
|
+
const textCmd = choudoufuLivePlanCommand({
|
|
1044
|
+
binary,
|
|
1045
|
+
...estateFlag,
|
|
1046
|
+
json: false,
|
|
1047
|
+
noColor: true,
|
|
1048
|
+
...(args.adoptionOnly ? { adoptionOnly: true } : {}),
|
|
1049
|
+
});
|
|
1050
|
+
// The human render carries `-detailed-exitcode` too, so a plan with changes
|
|
1051
|
+
// comes back as exit 2 on this call exactly as it does on the `-json` one.
|
|
1052
|
+
// Both are success, and the drift answer is already settled above, so exit 2
|
|
1053
|
+
// here yields its stdout rather than throwing; anything else is a failure
|
|
1054
|
+
// with choudoufu's own output attached.
|
|
1055
|
+
let text: string;
|
|
1056
|
+
try {
|
|
1057
|
+
text = (await run(textCmd, dir, env, signal)).stdout;
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
const failure = err as ExecFailure;
|
|
1060
|
+
if (failure.code !== 2) {
|
|
1061
|
+
const detail = (failure.stderr ?? "").trim() || (failure.stdout ?? "").trim();
|
|
1062
|
+
throw new Error(
|
|
1063
|
+
`${binary} live-plan (human render) failed in ${dir} (exit ${String(failure.code)})` +
|
|
1064
|
+
`${detail ? `\n${detail}` : ""}`,
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
text = failure.stdout ?? "";
|
|
1068
|
+
}
|
|
1031
1069
|
|
|
1032
1070
|
writeFileSync(join(dir, documentPath), document);
|
|
1033
1071
|
|
|
@@ -1050,7 +1088,7 @@ export async function choudoufuLivePlan(
|
|
|
1050
1088
|
documentPath,
|
|
1051
1089
|
estate,
|
|
1052
1090
|
...countLivePlanUnowned(json),
|
|
1053
|
-
...parseChoudoufuPlanSummary(
|
|
1091
|
+
...parseChoudoufuPlanSummary(text),
|
|
1054
1092
|
};
|
|
1055
1093
|
}
|
|
1056
1094
|
|
package/src/op/builders.ts
CHANGED
|
@@ -84,11 +84,13 @@ export const terraformApply = (
|
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* `choudoufu live-plan -detailed-exitcode -json
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
87
|
+
* `choudoufu live-plan -detailed-exitcode -json` in the named root, plus the
|
|
88
|
+
* human render. The fully typed twin of the `choudoufuLivePlan` activity
|
|
89
|
+
* (#2103). `opts` is {@link ChoudoufuLivePlanArgs} itself, minus the
|
|
90
|
+
* positional `root`; `estate` is optional, auto-detected from the root's
|
|
91
|
+
* `live` block or `estate.chdf.hcl` sidecar when omitted, and a root that
|
|
92
|
+
* declares one is run with no `-estate` at all, which is the only form
|
|
93
|
+
* choudoufu admits there (choudoufu #894, fixed in v0.14.0).
|
|
92
94
|
* Defaults to the `longInfra` profile: like `terraformPlan`, this reads the
|
|
93
95
|
* live system in full (the estate-wide sweep).
|
|
94
96
|
*
|
|
@@ -1,31 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Best-effort, synchronous read of whether a named `terraform.roots` entry is
|
|
3
3
|
* live: `terraform.binary` is `"choudoufu"` and the root's directory declares
|
|
4
|
-
* an estate
|
|
4
|
+
* an estate, either a `live { }` block or an `estate.chdf.hcl` sidecar (#2103).
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Three callers decide something at build time, before any activity runs, and
|
|
7
|
+
* none can afford the async work `../op/activities/terraform.ts`'s own
|
|
8
8
|
* `resolveRoot` does (`loadChantConfigUpward`, a full `chant.config.ts`/`.json`
|
|
9
9
|
* walk):
|
|
10
10
|
*
|
|
11
|
-
* - `TerraformApplyOp` (`../composites/terraform-apply-op.ts`)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* - `TerraformApplyOp` (`../composites/terraform-apply-op.ts`) reads the
|
|
12
|
+
* mode to word the Gate phase's approval description, and to refuse a
|
|
13
|
+
* root whose `policy` block sets `undeclared_untagged = "delete"`. The
|
|
14
|
+
* mode no longer picks a phase shape: #2157 made the live and stock Apply
|
|
15
|
+
* steps one step, so both roots build the same phases.
|
|
16
|
+
* - `TerraformWatchOp` (`../composites/terraform-watch-op.ts`) cross-checks
|
|
17
|
+
* its hand-set `live` flag against the root's real mode and refuses a
|
|
18
|
+
* mismatch (#2216).
|
|
14
19
|
* - TF101 (`../lint/rules/plan-before-apply.ts`) decides whether a
|
|
15
20
|
* `terraformApply` call it is statically inspecting is exempt from the
|
|
16
21
|
* plan-file pairing it otherwise enforces.
|
|
17
22
|
*
|
|
18
|
-
*
|
|
19
|
-
* an activity at run time, so a synchronous read is available, but only the
|
|
20
|
-
* cheap half of it: `findProjectConfig` (`@intentius/chant/project-root`) is a
|
|
21
|
-
* synchronous upward filesystem walk, but a `chant.config.ts`
|
|
22
|
-
* authored code, and evaluating it synchronously outside chant's
|
|
23
|
-
* config-sandbox machinery is more than
|
|
23
|
+
* All three run in the build process (`chant build`/`chant lint`) rather than
|
|
24
|
+
* in an activity at run time, so a synchronous read is available, but only the
|
|
25
|
+
* cheap half of it: `findProjectConfig` (`@intentius/chant/project-root`) is a
|
|
26
|
+
* plain, already-synchronous upward filesystem walk, but a `chant.config.ts`
|
|
27
|
+
* is project-authored code, and evaluating it synchronously outside chant's
|
|
28
|
+
* own config-sandbox machinery is more than any caller's best-effort question
|
|
24
29
|
* is worth. So this reads `chant.config.json` only; a project on a `.ts`
|
|
25
30
|
* config, or with no config discoverable at all, or naming no matching root,
|
|
26
|
-
* resolves to `undefined
|
|
27
|
-
*
|
|
28
|
-
*
|
|
31
|
+
* resolves to `undefined`, and "unknown" reads as "stock" to every caller.
|
|
32
|
+
*
|
|
33
|
+
* That default is conservative for the two callers that only ever add a
|
|
34
|
+
* requirement by knowing more: TF101 keeps enforcing the plan-file pairing,
|
|
35
|
+
* and the watch Op keeps accepting the flag it was given. It is the
|
|
36
|
+
* permissive direction for the apply Op's policy refusal and for the watch
|
|
37
|
+
* Op's mismatch, which is why neither refusal is left to this function alone:
|
|
38
|
+
* TF027 and TF028 (`../lint/post-synth/`) ask the same questions of the
|
|
39
|
+
* parsed HCL the build already stamped `mode` onto, whatever the config file
|
|
40
|
+
* is written in, and those checks are the guarantee (#2216). Every project in
|
|
41
|
+
* this repository is on a `chant.config.ts`, so a refusal that resolves the
|
|
42
|
+
* mode through this function alone never fires for anyone.
|
|
29
43
|
*/
|
|
30
44
|
|
|
31
45
|
import { existsSync, readFileSync } from "node:fs";
|
package/src/plugin.test.ts
CHANGED
|
@@ -337,3 +337,41 @@ describe("parseTerraformRootContent", () => {
|
|
|
337
337
|
expect([...entities.keys()]).toEqual(["app/locals", "app/locals~2"]);
|
|
338
338
|
});
|
|
339
339
|
});
|
|
340
|
+
|
|
341
|
+
describe("detectTemplate", () => {
|
|
342
|
+
const detect = (data: unknown) => terraformPlugin.detectTemplate?.(data) ?? false;
|
|
343
|
+
|
|
344
|
+
it("recognises .tf text by its top-level block headers", () => {
|
|
345
|
+
expect(detect('terraform {\n required_version = ">= 1.5.0"\n}')).toBe(true);
|
|
346
|
+
expect(detect('resource "null_resource" "first" {}')).toBe(true);
|
|
347
|
+
expect(detect('data "aws_ami" "ubuntu" {\n most_recent = true\n}')).toBe(true);
|
|
348
|
+
expect(detect('provider "aws" {\n region = "us-east-1"\n}')).toBe(true);
|
|
349
|
+
expect(detect('module "vpc" {\n source = "./modules/vpc"\n}')).toBe(true);
|
|
350
|
+
expect(detect('variable "region" {\n type = string\n}')).toBe(true);
|
|
351
|
+
expect(detect('output "id" {\n value = null_resource.first.id\n}')).toBe(true);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("recognises the .tf.json syntax by its whole key set", () => {
|
|
355
|
+
expect(detect({ resource: { null_resource: { first: {} } } })).toBe(true);
|
|
356
|
+
expect(detect({ terraform: { required_version: ">= 1.5.0" }, provider: { aws: {} } })).toBe(true);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("refuses another lexicon's document, and anything with no block at all", () => {
|
|
360
|
+
// k8s / fountain / gcp, which share the apiVersion+kind shape.
|
|
361
|
+
expect(detect({ apiVersion: "v1", kind: "ConfigMap", data: {} })).toBe(false);
|
|
362
|
+
// CloudFormation, whose `Resources` is not one of Terraform's block names.
|
|
363
|
+
expect(detect({ AWSTemplateFormatVersion: "2010-09-09", Resources: {} })).toBe(false);
|
|
364
|
+
// Compose.
|
|
365
|
+
expect(detect({ services: { web: { image: "nginx" } } })).toBe(false);
|
|
366
|
+
expect(detect("# just a comment\n")).toBe(false);
|
|
367
|
+
expect(detect("")).toBe(false);
|
|
368
|
+
expect(detect({})).toBe(false);
|
|
369
|
+
expect(detect(null)).toBe(false);
|
|
370
|
+
expect(detect([{ resource: {} }])).toBe(false);
|
|
371
|
+
expect(detect(42)).toBe(false);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("does not match a block header that is only mentioned mid-line", () => {
|
|
375
|
+
expect(detect('# see resource "null_resource" "first" {} in main.tf')).toBe(false);
|
|
376
|
+
});
|
|
377
|
+
});
|
package/src/plugin.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LexiconPlugin } from "@intentius/chant/lexicon";
|
|
1
|
+
import type { AuditEntitiesInput, LexiconPlugin } from "@intentius/chant/lexicon";
|
|
2
2
|
import type { CompletionContext, HoverContext } from "@intentius/chant/lsp/types";
|
|
3
3
|
import type { Declarable } from "@intentius/chant/declarable";
|
|
4
4
|
import { createSkillsLoader } from "@intentius/chant/lexicon-plugin-helpers";
|
|
@@ -11,7 +11,8 @@ import { completions } from "./lsp/completions";
|
|
|
11
11
|
import { hover } from "./lsp/hover";
|
|
12
12
|
import { terraformConfigSchema, type TerraformConfig } from "./config";
|
|
13
13
|
import { renderTerraformRoots } from "./hcl/roots";
|
|
14
|
-
import { parseTerraformRootContent, RESOURCE_TYPE } from "./hcl/parse";
|
|
14
|
+
import { auditRootName, parseTerraformRootContent, RESOURCE_TYPE } from "./hcl/parse";
|
|
15
|
+
import { descendModules } from "./hcl/descend";
|
|
15
16
|
import { TERRAFORM_STATE_OWNERSHIP_KEYS } from "./state-ownership";
|
|
16
17
|
|
|
17
18
|
const loadSkills = createSkillsLoader(import.meta.url, [
|
|
@@ -23,6 +24,43 @@ const loadSkills = createSkillsLoader(import.meta.url, [
|
|
|
23
24
|
},
|
|
24
25
|
]);
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The only top-level names Terraform's JSON syntax admits
|
|
29
|
+
* (developer.hashicorp.com/terraform/language/syntax/json). Used as a
|
|
30
|
+
* whole-document test in `detectTemplate`: a `.tf.json` file has these keys
|
|
31
|
+
* and nothing else.
|
|
32
|
+
*/
|
|
33
|
+
const TF_JSON_BLOCK_TYPES = new Set([
|
|
34
|
+
"terraform",
|
|
35
|
+
"provider",
|
|
36
|
+
"variable",
|
|
37
|
+
"output",
|
|
38
|
+
"locals",
|
|
39
|
+
"module",
|
|
40
|
+
"resource",
|
|
41
|
+
"data",
|
|
42
|
+
"moved",
|
|
43
|
+
"import",
|
|
44
|
+
"check",
|
|
45
|
+
"removed",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether raw text is HCL a Terraform root or module is written in. One
|
|
50
|
+
* top-level block header is enough, and every one of these headers is
|
|
51
|
+
* Terraform's own: `terraform {`, and the labelled `resource`/`data`
|
|
52
|
+
* (two labels), `provider`/`module`/`variable`/`output` (one label). `locals`
|
|
53
|
+
* is left out on purpose, since it is the one block a non-Terraform HCL
|
|
54
|
+
* dialect is also likely to carry.
|
|
55
|
+
*/
|
|
56
|
+
function looksLikeHcl(text: string): boolean {
|
|
57
|
+
return (
|
|
58
|
+
/^[ \t]*terraform[ \t]*\{/m.test(text) ||
|
|
59
|
+
/^[ \t]*(resource|data)[ \t]+"[^"]+"[ \t]+"[^"]+"[ \t]*\{/m.test(text) ||
|
|
60
|
+
/^[ \t]*(provider|module|variable|output)[ \t]+"[^"]+"[ \t]*\{/m.test(text)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
26
64
|
/**
|
|
27
65
|
* terraform lexicon plugin.
|
|
28
66
|
*
|
|
@@ -166,46 +204,99 @@ export const terraformPlugin: LexiconPlugin = {
|
|
|
166
204
|
},
|
|
167
205
|
|
|
168
206
|
/**
|
|
169
|
-
* Parse-to-graph for `chant audit` (#1567, #2085). `content` is the
|
|
207
|
+
* Parse-to-graph for `chant audit` (#1567, #2085, #2217). `content` is the
|
|
170
208
|
* `# file: <name>`-joined bundle discovery builds for one discovered root
|
|
171
|
-
* module (`classifyTerraform`, `packages/core/src/audit/
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
209
|
+
* module (`classifyTerraform`, `packages/core/src/audit/discover.ts`), and
|
|
210
|
+
* `input` says where that bundle came from.
|
|
211
|
+
*
|
|
212
|
+
* The scope is named after the input's path (`auditRootName`), so two roots
|
|
213
|
+
* in one repository are two roots here as well: they no longer collide in
|
|
214
|
+
* the merged entity map the all-files pass reads, which is what used to
|
|
215
|
+
* produce a finding keyed `<root>/<address>#2` against the file
|
|
216
|
+
* `(cross-file)` that neither directory deserved.
|
|
217
|
+
*
|
|
218
|
+
* When the audit walked a local filesystem, `input.dir` is the root
|
|
219
|
+
* module's directory and the parse descends its local `module` calls
|
|
220
|
+
* exactly as `buildRoots()` does, through the same `descendModules`
|
|
221
|
+
* (`../hcl/descend.ts`): a child module's entities are keyed
|
|
222
|
+
* `<root>/module.<name>/<address>` and carry the caller chain, so TF014 and
|
|
223
|
+
* TF015 fire here with the same `Callers:` line they print on a build, and
|
|
224
|
+
* the root-only rules (TF001 to TF003) still see only the root's own
|
|
225
|
+
* blocks. `input.baseDir` is the audited directory, which bounds the
|
|
226
|
+
* descent the way the project root bounds it on a build. Discovery drops a
|
|
227
|
+
* directory another one calls as a local module, so no module is audited
|
|
228
|
+
* twice.
|
|
229
|
+
*
|
|
230
|
+
* With no `input.dir` (a fetched repository; a caller parsing a bare
|
|
231
|
+
* string) the bundle is parsed alone, and the two child-module rules have
|
|
232
|
+
* no child to report on, since nothing named one.
|
|
233
|
+
*
|
|
234
|
+
* The descent's own refusals (a registry source, a source outside the
|
|
235
|
+
* audited tree, a cycle) are warnings on a build; this hook returns
|
|
236
|
+
* entities only, so on the audit path they are dropped rather than printed.
|
|
177
237
|
*
|
|
178
|
-
*
|
|
179
|
-
* `blocksToEntities`, but finds nothing to descend into: this hook is
|
|
180
|
-
* handed one directory's joined text and no directory to read a local
|
|
181
|
-
* module's files from, and audit discovery has already classified every
|
|
182
|
-
* nested `.tf` directory as an audit input of its own (`classifyTerraform`,
|
|
183
|
-
* `packages/core/src/audit/discover.ts`). So a child module IS audited on
|
|
184
|
-
* this path, as its own root rather than as a child scope, which means the
|
|
185
|
-
* two rules that ask "is this block inside a child module?" (TF014, TF015)
|
|
186
|
-
* report on `chant build` and not in `chant audit`, and TF020 answers per
|
|
187
|
-
* directory either way. The rule pages say so.
|
|
238
|
+
* Never throws: malformed HCL yields an empty map.
|
|
188
239
|
*/
|
|
189
|
-
async auditEntities(content: string): Promise<Map<string, Declarable>> {
|
|
240
|
+
async auditEntities(content: string, input?: AuditEntitiesInput): Promise<Map<string, Declarable>> {
|
|
190
241
|
try {
|
|
191
|
-
|
|
242
|
+
const root = auditRootName(input?.path);
|
|
243
|
+
const entities = await parseTerraformRootContent(content, root);
|
|
244
|
+
if (input?.dir === undefined) return entities;
|
|
245
|
+
const { entities: children } = await descendModules(entities, {
|
|
246
|
+
dir: input.dir,
|
|
247
|
+
root,
|
|
248
|
+
projectRoot: input.baseDir ?? input.dir,
|
|
249
|
+
});
|
|
250
|
+
for (const [key, entity] of children) entities.set(key, entity);
|
|
251
|
+
return entities;
|
|
192
252
|
} catch {
|
|
193
253
|
return new Map();
|
|
194
254
|
}
|
|
195
255
|
},
|
|
196
256
|
|
|
257
|
+
/**
|
|
258
|
+
* No `mcpTools()` and no `mcpResources()`, deliberately (#2220). Both were
|
|
259
|
+
* declared returning `[]` behind a TODO, which passed the two tier-2
|
|
260
|
+
* "registers <method>" checks on a member that contributed nothing. Neither
|
|
261
|
+
* of the two shapes every other lexicon uses has anything to serve here:
|
|
262
|
+
* `createDiffTool` diffs one build's serializer output against the last,
|
|
263
|
+
* and this serializer writes nothing (`src/serializer.ts` is a deliberate
|
|
264
|
+
* no-op, the `.tf` files are the artifact); `createCatalogResource` serves
|
|
265
|
+
* the generated resource registry, and this lexicon generates none
|
|
266
|
+
* (`src/codegen/generate.ts` writes `{}` on purpose). So the members are
|
|
267
|
+
* removed rather than faked, and `chant dev check-lexicon` reports two
|
|
268
|
+
* tier-2 gaps that are real.
|
|
269
|
+
*/
|
|
197
270
|
skills: loadSkills,
|
|
198
271
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
272
|
+
/**
|
|
273
|
+
* `.tf` (and Terraform's own JSON syntax, `.tf.json`) recognised by content
|
|
274
|
+
* rather than by which directory a file sits in (#2220).
|
|
275
|
+
*
|
|
276
|
+
* Two input shapes, because the two callers hand over different things.
|
|
277
|
+
* `chant import` (`packages/core/src/cli/commands/import.ts`) `JSON.parse`s
|
|
278
|
+
* the file first, so it arrives as an object; the audit's content detection
|
|
279
|
+
* (`packages/core/src/audit/discover.ts`) hands azure its raw string, and a
|
|
280
|
+
* `.tf` file is not JSON at all, so the raw string form is the one that
|
|
281
|
+
* matters for HCL. Anything else reads as false.
|
|
282
|
+
*
|
|
283
|
+
* The object form is exact rather than heuristic: Terraform's JSON syntax
|
|
284
|
+
* admits only the top-level names in {@link TF_JSON_BLOCK_TYPES}, so a
|
|
285
|
+
* document is `.tf.json` when every key is one of them and at least one is
|
|
286
|
+
* present. That refuses a Kubernetes manifest, a CloudFormation template and
|
|
287
|
+
* a Compose file without having to name any of them.
|
|
288
|
+
*
|
|
289
|
+
* Discovery still classifies Terraform by the directory bundle
|
|
290
|
+
* (`classifyTerraform`), because a root module is a directory of files that
|
|
291
|
+
* has to be parsed together and one `.tf` on its own is not an audit input.
|
|
292
|
+
* This makes the per-file question answerable for the callers that ask it
|
|
293
|
+
* one file at a time.
|
|
294
|
+
*/
|
|
295
|
+
detectTemplate(data: unknown) {
|
|
296
|
+
if (typeof data === "string") return looksLikeHcl(data);
|
|
297
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return false;
|
|
298
|
+
const keys = Object.keys(data as Record<string, unknown>);
|
|
299
|
+
return keys.length > 0 && keys.every((k) => TF_JSON_BLOCK_TYPES.has(k));
|
|
209
300
|
},
|
|
210
301
|
|
|
211
302
|
completionProvider(ctx: CompletionContext) {
|