@intentius/chant-lexicon-terraform 0.124.45 → 0.124.48
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__/capabilities/chant.config.d.ts +26 -0
- package/dist/__fixtures__/capabilities/chant.config.d.ts.map +1 -0
- package/dist/__fixtures__/capabilities/chant.config.js +13 -0
- package/dist/__fixtures__/effective-topology/chant.config.d.ts +13 -0
- package/dist/__fixtures__/effective-topology/chant.config.d.ts.map +1 -0
- package/dist/__fixtures__/effective-topology/chant.config.js +9 -0
- package/dist/__fixtures__/kind-type/chant.config.d.ts +13 -0
- package/dist/__fixtures__/kind-type/chant.config.d.ts.map +1 -0
- package/dist/__fixtures__/kind-type/chant.config.js +9 -0
- package/dist/__fixtures__/search-roots/chant.config.d.ts +13 -0
- package/dist/__fixtures__/search-roots/chant.config.d.ts.map +1 -0
- package/dist/__fixtures__/search-roots/chant.config.js +9 -0
- package/dist/__fixtures__/sensitive-default/chant.config.d.ts +13 -0
- package/dist/__fixtures__/sensitive-default/chant.config.d.ts.map +1 -0
- package/dist/__fixtures__/sensitive-default/chant.config.js +9 -0
- package/dist/capabilities/adapter.d.ts +9 -0
- package/dist/capabilities/adapter.d.ts.map +1 -0
- package/dist/capabilities/adapter.js +25 -0
- package/dist/capabilities/mapping.json +166 -0
- package/dist/config.d.ts +52 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +23 -0
- package/dist/describe-resources.d.ts +101 -8
- package/dist/describe-resources.d.ts.map +1 -1
- package/dist/describe-resources.js +395 -56
- package/dist/hcl/parse.d.ts +10 -0
- package/dist/hcl/parse.d.ts.map +1 -1
- package/dist/hcl/parse.js +20 -2
- package/dist/hcl/redact.d.ts +26 -0
- package/dist/hcl/redact.d.ts.map +1 -0
- package/dist/hcl/redact.js +228 -0
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/op/activities/terraform.d.ts +109 -4
- package/dist/op/activities/terraform.d.ts.map +1 -1
- package/dist/op/activities/terraform.js +148 -7
- package/dist/plan-observation.d.ts +153 -0
- package/dist/plan-observation.d.ts.map +1 -0
- package/dist/plan-observation.js +354 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +2 -0
- package/package.json +2 -2
- package/src/__fixtures__/ACCEPTANCE.md +53 -1
- package/src/__fixtures__/capabilities/catalog/main.tf +105 -0
- package/src/__fixtures__/capabilities/chant.config.ts +15 -0
- package/src/__fixtures__/capabilities/findings/main.tf +225 -0
- package/src/__fixtures__/capabilities/ledger/main.tf +265 -0
- package/src/__fixtures__/capabilities/package.json +1 -0
- package/src/__fixtures__/effective-topology/chant.config.ts +11 -0
- package/src/__fixtures__/effective-topology/net/main.tf +176 -0
- package/src/__fixtures__/effective-topology/package.json +6 -0
- package/src/__fixtures__/kind-type/app/main.tf +35 -0
- package/src/__fixtures__/kind-type/chant.config.ts +11 -0
- package/src/__fixtures__/kind-type/package.json +6 -0
- package/src/__fixtures__/live-plan-depth/README.md +59 -0
- package/src/__fixtures__/live-plan-depth/live-plan.json +110 -0
- package/src/__fixtures__/live-plan-depth/show-plan-sensitive.json +1 -0
- package/src/__fixtures__/live-plan-depth/show-plan.json +1 -0
- package/src/__fixtures__/plan-read/main.tf +46 -0
- package/src/__fixtures__/plan-read/next.tfvars +1 -0
- package/src/__fixtures__/plan-read-drift/README.md +33 -0
- package/src/__fixtures__/plan-read-drift/main.tf +63 -0
- package/src/__fixtures__/plan-read-drift/next.tfvars +1 -0
- package/src/__fixtures__/search-roots/chant.config.ts +11 -0
- package/src/__fixtures__/search-roots/net/main.tf +73 -0
- package/src/__fixtures__/search-roots/package.json +6 -0
- package/src/__fixtures__/sensitive-default/chant.config.ts +11 -0
- package/src/__fixtures__/sensitive-default/net/main.tf +38 -0
- package/src/__fixtures__/sensitive-default/package.json +6 -0
- package/src/__fixtures__/show-plan.json +663 -0
- package/src/capabilities/adapter.ts +32 -0
- package/src/capabilities/mapping.json +166 -0
- package/src/capabilities-workspace.e2e.test.ts +251 -0
- package/src/capabilities.test.ts +264 -0
- package/src/config.ts +26 -0
- package/src/describe-resources.live-plan.test.ts +302 -0
- package/src/describe-resources.live-root-unchanged.test.ts +126 -0
- package/src/describe-resources.plan.test.ts +437 -0
- package/src/describe-resources.ts +445 -52
- package/src/effective-topology.test.ts +134 -0
- package/src/hcl/parse.ts +30 -2
- package/src/hcl/redact.ts +181 -0
- package/src/live-plan-depth.acceptance.e2e.test.ts +193 -0
- package/src/op/activities/terraform.test.ts +118 -1
- package/src/op/activities/terraform.ts +213 -9
- package/src/plan-observation.ts +428 -0
- package/src/plan-read.acceptance.test.ts +184 -0
- package/src/plugin.ts +2 -0
- package/src/redact-ir.e2e.test.ts +79 -0
- package/src/redact.test.ts +111 -0
- package/src/search-build-roots.test.ts +125 -0
- package/src/search-count.test.ts +144 -0
- package/src/search-kind-type.test.ts +136 -0
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { describe, test, expect, beforeEach, afterAll, vi } from "vitest";
|
|
17
|
-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { existsSync, mkdtempSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { tmpdir } from "node:os";
|
|
19
19
|
import { join, resolve } from "node:path";
|
|
20
20
|
import {
|
|
@@ -22,6 +22,13 @@ import {
|
|
|
22
22
|
terraformPlan,
|
|
23
23
|
terraformApply,
|
|
24
24
|
terraformShow,
|
|
25
|
+
terraformPlanRead,
|
|
26
|
+
terraformPlanReadCommand,
|
|
27
|
+
terraformInitLine,
|
|
28
|
+
classifyPlanReadFailure,
|
|
29
|
+
rootIsInitialised,
|
|
30
|
+
TerraformPlanReadError,
|
|
31
|
+
PLAN_READ_DIR_PREFIX,
|
|
25
32
|
choudoufuLivePlan,
|
|
26
33
|
choudoufuLivePlanDocument,
|
|
27
34
|
choudoufuLiveLs,
|
|
@@ -506,6 +513,116 @@ describe("terraformApply — saved plans only (#2086)", () => {
|
|
|
506
513
|
});
|
|
507
514
|
});
|
|
508
515
|
|
|
516
|
+
// ── terraformPlanRead (#4358) ───────────────────────────────────────────────
|
|
517
|
+
|
|
518
|
+
/** Every entry under `dir`, recursively, as relative paths. */
|
|
519
|
+
function tree(dir: string): string[] {
|
|
520
|
+
return readdirSync(dir, { recursive: true }).map(String).sort();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
describe("terraformPlanReadCommand (#4358)", () => {
|
|
524
|
+
test("always carries -lock=false, with -input=false and the plan file outside the root", () => {
|
|
525
|
+
const cmd = terraformPlanReadCommand({ binary: "terraform", planFile: "/tmp/chant-terraform-plan-x/plan.tfplan" });
|
|
526
|
+
expect(cmd.split(" ")).toContain("-lock=false");
|
|
527
|
+
expect(cmd).toBe("terraform plan -input=false -lock=false -out=/tmp/chant-terraform-plan-x/plan.tfplan");
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("carries the root's var files, in order, before -out", () => {
|
|
531
|
+
expect(terraformPlanReadCommand({ binary: "tofu", planFile: "/t/p", varFiles: ["a.tfvars", "b.tfvars"] })).toBe(
|
|
532
|
+
"tofu plan -input=false -lock=false -var-file=a.tfvars -var-file=b.tfvars -out=/t/p",
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
describe("terraformInitLine (#4358)", () => {
|
|
538
|
+
test("names the root by -chdir and carries its backend config, so it pastes as printed", () => {
|
|
539
|
+
expect(terraformInitLine({ binary: "terraform", dir: "/p/infra", backendConfig: { bucket: "b" } })).toBe(
|
|
540
|
+
"terraform -chdir=/p/infra init -input=false -backend-config=bucket=b",
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
describe("classifyPlanReadFailure (#4358)", () => {
|
|
546
|
+
test.each([
|
|
547
|
+
["Error: Backend initialization required, please run \"terraform init\"", "not-initialised"],
|
|
548
|
+
["Error: Inconsistent dependency lock file", "not-initialised"],
|
|
549
|
+
["Error: Required plugins are not installed", "not-initialised"],
|
|
550
|
+
["Error: Failed to get existing workspaces: AccessDenied", "backend"],
|
|
551
|
+
["Error: error configuring S3 Backend: no valid credential sources for S3 Backend found.", "backend"],
|
|
552
|
+
["Error: error loading state: storage: bucket doesn't exist", "backend"],
|
|
553
|
+
["Error: No valid credential sources found", "plan"],
|
|
554
|
+
["Error: Invalid provider configuration", "plan"],
|
|
555
|
+
])("%s -> %s", (output, kind) => {
|
|
556
|
+
expect(classifyPlanReadFailure(output)).toBe(kind);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
describe("terraformPlanRead (#4358)", () => {
|
|
561
|
+
function initialisedProject(): string {
|
|
562
|
+
const dir = project({ roots: { app: { dir: "./infra", varFiles: ["next.tfvars"] } } });
|
|
563
|
+
mkdirSync(join(dir, "infra", ".terraform"));
|
|
564
|
+
return dir;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
test("plans with -lock=false into a temp dir, shows it, and removes the temp dir", async () => {
|
|
568
|
+
const dir = initialisedProject();
|
|
569
|
+
const before = tree(join(dir, "infra"));
|
|
570
|
+
replies.push({ match: "show -json", reply: { stdout: '{"format_version":"1.2"}', stderr: "" } });
|
|
571
|
+
const result = await terraformPlanRead({ root: "app", cwd: dir });
|
|
572
|
+
|
|
573
|
+
const [plan, show] = execCalls.map((c) => c.cmd);
|
|
574
|
+
expect(plan).toContain(" -lock=false ");
|
|
575
|
+
expect(plan).toBe(`terraform plan -input=false -lock=false -var-file=next.tfvars -out=${join(result.planDir, "plan.tfplan")}`);
|
|
576
|
+
expect(show).toBe(`terraform show -json ${join(result.planDir, "plan.tfplan")}`);
|
|
577
|
+
expect(execCalls[0].opts.cwd).toBe(resolve(dir, "infra"));
|
|
578
|
+
expect(execCalls[0].opts.env?.TF_IN_AUTOMATION).toBe("1");
|
|
579
|
+
|
|
580
|
+
expect(result.planDir.startsWith(join(tmpdir(), PLAN_READ_DIR_PREFIX))).toBe(true);
|
|
581
|
+
expect(existsSync(result.planDir)).toBe(false);
|
|
582
|
+
expect(tree(join(dir, "infra"))).toEqual(before);
|
|
583
|
+
expect(result.json).toEqual({ format_version: "1.2" });
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
test("removes the temp dir when the plan fails, too", async () => {
|
|
587
|
+
const dir = initialisedProject();
|
|
588
|
+
replies.push({ match: "plan -input=false", reply: execError(1, "Error: No valid credential sources found") });
|
|
589
|
+
const err = await terraformPlanRead({ root: "app", cwd: dir }).catch((e: unknown) => e);
|
|
590
|
+
expect(err).toBeInstanceOf(TerraformPlanReadError);
|
|
591
|
+
expect((err as TerraformPlanReadError).kind).toBe("plan");
|
|
592
|
+
expect((err as TerraformPlanReadError).message).toContain("Error: No valid credential sources found");
|
|
593
|
+
const planFile = execCalls[0].cmd.split("-out=")[1]!;
|
|
594
|
+
expect(existsSync(planFile)).toBe(false);
|
|
595
|
+
expect(existsSync(join(planFile, ".."))).toBe(false);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("refuses an uninitialised root with the init line, and runs nothing", async () => {
|
|
599
|
+
const dir = project({ roots: { app: { dir: "./infra", backendConfig: { key: "app.tfstate" } } } });
|
|
600
|
+
const err = (await terraformPlanRead({ root: "app", cwd: dir }).catch((e: unknown) => e)) as TerraformPlanReadError;
|
|
601
|
+
expect(err).toBeInstanceOf(TerraformPlanReadError);
|
|
602
|
+
expect(err.kind).toBe("not-initialised");
|
|
603
|
+
expect(err.initLine).toBe(`terraform -chdir=${resolve(dir, "infra")} init -input=false -backend-config=key=app.tfstate`);
|
|
604
|
+
expect(err.message).toContain(`run \`${err.initLine}\` first`);
|
|
605
|
+
expect(execCalls).toHaveLength(0);
|
|
606
|
+
expect(tree(join(dir, "infra"))).toEqual([]);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("a plan that says the root needs init again is refused the same way", async () => {
|
|
610
|
+
const dir = initialisedProject();
|
|
611
|
+
replies.push({ match: "plan -input=false", reply: execError(1, "Error: Inconsistent dependency lock file") });
|
|
612
|
+
const err = (await terraformPlanRead({ root: "app", cwd: dir }).catch((e: unknown) => e)) as TerraformPlanReadError;
|
|
613
|
+
expect(err.kind).toBe("not-initialised");
|
|
614
|
+
expect(err.message).toContain("init -input=false");
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("rootIsInitialised honours TF_DATA_DIR", () => {
|
|
618
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-tf-init-"));
|
|
619
|
+
workspaces.push(dir);
|
|
620
|
+
expect(rootIsInitialised(dir, {})).toBe(false);
|
|
621
|
+
mkdirSync(join(dir, "data"));
|
|
622
|
+
expect(rootIsInitialised(dir, { TF_DATA_DIR: "data" })).toBe(true);
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
509
626
|
// ── terraformShow ───────────────────────────────────────────────────────────
|
|
510
627
|
|
|
511
628
|
describe("terraformShow (#2086)", () => {
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
import { exec } from "node:child_process";
|
|
43
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
43
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
44
|
+
import { tmpdir } from "node:os";
|
|
44
45
|
import { promisify } from "node:util";
|
|
45
46
|
import { resolve, dirname, join, relative, sep } from "node:path";
|
|
46
47
|
import { loadChantConfigUpward } from "@intentius/chant/config";
|
|
@@ -72,7 +73,7 @@ export const DEFAULT_PLAN_FILE = "chant.tfplan";
|
|
|
72
73
|
/** Binary used when the `terraform` namespace records no preference. */
|
|
73
74
|
export const DEFAULT_TERRAFORM_BINARY = "terraform";
|
|
74
75
|
|
|
75
|
-
/** Where {@link choudoufuLivePlan} writes GitHub issue #788's JSON document, relative to the root directory. */
|
|
76
|
+
/** Where the {@link choudoufuLivePlan} Op activity writes GitHub issue #788's JSON document, relative to the root directory. */
|
|
76
77
|
export const DEFAULT_LIVE_PLAN_DOCUMENT_FILE = "chant.live-plan.json";
|
|
77
78
|
|
|
78
79
|
/**
|
|
@@ -1484,6 +1485,190 @@ export async function terraformShow(
|
|
|
1484
1485
|
};
|
|
1485
1486
|
}
|
|
1486
1487
|
|
|
1488
|
+
// ── The plan read (#4358) ───────────────────────────────────────────────────
|
|
1489
|
+
|
|
1490
|
+
/** Prefix of the scratch directory a plan read writes its plan file into, under the OS temp directory. */
|
|
1491
|
+
export const PLAN_READ_DIR_PREFIX = "chant-terraform-plan-";
|
|
1492
|
+
|
|
1493
|
+
/** The plan file's name inside that scratch directory. */
|
|
1494
|
+
export const PLAN_READ_FILE = "plan.tfplan";
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* `terraform plan` for an observation read (#4358): `-input=false -lock=false
|
|
1498
|
+
* -out=<file>`, plus the root's own `-var-file`s.
|
|
1499
|
+
*
|
|
1500
|
+
* `-lock=false` is the point. A read must never take the backend's state lock,
|
|
1501
|
+
* or a teammate's apply that starts while chant is reading would fail on
|
|
1502
|
+
* "Error acquiring the state lock". `-detailed-exitcode` is not passed: the
|
|
1503
|
+
* read wants the document, not the boolean, so exit 0 is the only success.
|
|
1504
|
+
* The `-out` path is always outside the root (see {@link terraformPlanRead}).
|
|
1505
|
+
*/
|
|
1506
|
+
export function terraformPlanReadCommand(opts: { binary: string; planFile: string; varFiles?: string[] }): string {
|
|
1507
|
+
const parts = [opts.binary, "plan", "-input=false", "-lock=false"];
|
|
1508
|
+
for (const varFile of opts.varFiles ?? []) parts.push(`-var-file=${quoteArg(varFile)}`);
|
|
1509
|
+
parts.push(`-out=${quoteArg(opts.planFile)}`);
|
|
1510
|
+
return parts.join(" ");
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* The one line a person runs to initialise a root chant refuses to plan,
|
|
1515
|
+
* with the root's `backendConfig` carried, so it can be pasted as printed.
|
|
1516
|
+
*/
|
|
1517
|
+
export function terraformInitLine(opts: { binary: string; dir: string; backendConfig?: Record<string, string> }): string {
|
|
1518
|
+
const init = terraformInitCommand({
|
|
1519
|
+
binary: opts.binary,
|
|
1520
|
+
...(opts.backendConfig ? { backendConfig: opts.backendConfig } : {}),
|
|
1521
|
+
});
|
|
1522
|
+
return init.replace(`${opts.binary} init`, `${opts.binary} -chdir=${quoteArg(opts.dir)} init`);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/**
|
|
1526
|
+
* Why a plan read refused or failed, in the three classes the plan depth
|
|
1527
|
+
* falls back by (#4358):
|
|
1528
|
+
*
|
|
1529
|
+
* - `"not-initialised"`: the root was never initialised, or its providers,
|
|
1530
|
+
* modules or backend changed since. chant never runs `init` on a read,
|
|
1531
|
+
* because `init` writes into the root, so this is refused with the line
|
|
1532
|
+
* to run.
|
|
1533
|
+
* - `"backend"`: the state could not be read at all. A state read needs the
|
|
1534
|
+
* same backend, so there is nothing to fall back to but declared only.
|
|
1535
|
+
* - `"plan"`: anything else, missing provider credentials first among them.
|
|
1536
|
+
* The backend answered, so the state read can still answer.
|
|
1537
|
+
*/
|
|
1538
|
+
export type TerraformPlanReadFailureKind = "not-initialised" | "backend" | "plan";
|
|
1539
|
+
|
|
1540
|
+
/** A plan read that did not produce a document. `kind` decides the fallback. */
|
|
1541
|
+
export class TerraformPlanReadError extends Error {
|
|
1542
|
+
readonly kind: TerraformPlanReadFailureKind;
|
|
1543
|
+
readonly dir: string;
|
|
1544
|
+
/** Present on `"not-initialised"`: the line to run. */
|
|
1545
|
+
readonly initLine?: string;
|
|
1546
|
+
|
|
1547
|
+
constructor(kind: TerraformPlanReadFailureKind, message: string, dir: string, initLine?: string) {
|
|
1548
|
+
super(message);
|
|
1549
|
+
this.name = "TerraformPlanReadError";
|
|
1550
|
+
this.kind = kind;
|
|
1551
|
+
this.dir = dir;
|
|
1552
|
+
if (initLine !== undefined) this.initLine = initLine;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
/**
|
|
1557
|
+
* Sort a failed plan's own output into a {@link TerraformPlanReadFailureKind}.
|
|
1558
|
+
* Matched on terraform's and OpenTofu's own error summaries. Order matters:
|
|
1559
|
+
* an S3 backend without credentials says both "backend" and "credentials",
|
|
1560
|
+
* and it is the backend that failed, so the backend patterns are read first.
|
|
1561
|
+
*/
|
|
1562
|
+
export function classifyPlanReadFailure(output: string): TerraformPlanReadFailureKind {
|
|
1563
|
+
if (
|
|
1564
|
+
/Backend initialization required|Inconsistent dependency lock file|Required plugins are not installed|Module not installed|Missing required provider|please run "(terraform|tofu|choudoufu) init"|Run "(terraform|tofu|choudoufu) init"|Backend configuration changed/i.test(
|
|
1565
|
+
output,
|
|
1566
|
+
)
|
|
1567
|
+
) {
|
|
1568
|
+
return "not-initialised";
|
|
1569
|
+
}
|
|
1570
|
+
if (
|
|
1571
|
+
/error loading state|Failed to load state|Error refreshing state|Failed to get existing workspaces|error configuring (the )?\S+ backend|Error: Failed to (read|retrieve) state|Error retrieving state|Error acquiring the state lock|state data in S3|NoSuchBucket|failed to (get|read) (remote )?state|Unable to access object|\bbackend\b.*(credential|denied|unauthori[sz]ed|forbidden)/i.test(
|
|
1572
|
+
output,
|
|
1573
|
+
)
|
|
1574
|
+
) {
|
|
1575
|
+
return "backend";
|
|
1576
|
+
}
|
|
1577
|
+
return "plan";
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/** What {@link terraformPlanRead} resolved. */
|
|
1581
|
+
export interface TerraformPlanReadResult {
|
|
1582
|
+
/** `terraform show -json <planfile>`, parsed. */
|
|
1583
|
+
json: unknown;
|
|
1584
|
+
/** Absolute path of the root module directory the plan ran in. */
|
|
1585
|
+
dir: string;
|
|
1586
|
+
/**
|
|
1587
|
+
* The scratch directory the plan file was written to, under the OS temp
|
|
1588
|
+
* directory. Already removed when this result is returned; named so a test
|
|
1589
|
+
* can prove both halves of that.
|
|
1590
|
+
*/
|
|
1591
|
+
planDir: string;
|
|
1592
|
+
/** The plan command exactly as run, for a test or a note to quote. */
|
|
1593
|
+
command: string;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Whether a root has been initialised: `.terraform` (or `TF_DATA_DIR`, when
|
|
1598
|
+
* the environment points elsewhere) exists. A root whose providers or modules
|
|
1599
|
+
* have since changed passes this check and is caught by
|
|
1600
|
+
* {@link classifyPlanReadFailure} on the plan's own error instead.
|
|
1601
|
+
*/
|
|
1602
|
+
export function rootIsInitialised(dir: string, env: Record<string, string | undefined> = process.env): boolean {
|
|
1603
|
+
const dataDir = env.TF_DATA_DIR ? resolve(dir, env.TF_DATA_DIR) : join(dir, ".terraform");
|
|
1604
|
+
return existsSync(dataDir);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/**
|
|
1608
|
+
* Read a saved plan for one root without writing anything in it (#4358):
|
|
1609
|
+
* `plan -input=false -lock=false -out=<tmp>/plan.tfplan` in the root, then
|
|
1610
|
+
* `show -json <tmp>/plan.tfplan`, then remove `<tmp>`.
|
|
1611
|
+
*
|
|
1612
|
+
* Three rules, each a lesson behold already learned reading choudoufu
|
|
1613
|
+
* members (behold `src/choudoufu-plan.ts`):
|
|
1614
|
+
*
|
|
1615
|
+
* - No lock. `-lock=false` on the plan, always ({@link terraformPlanReadCommand}).
|
|
1616
|
+
* - Nothing in the root. `<tmp>` is a fresh `mkdtemp` under the OS temp
|
|
1617
|
+
* directory and is removed in a `finally`, failure or not. `init` is never
|
|
1618
|
+
* run: an uninitialised root throws a {@link TerraformPlanReadError} of
|
|
1619
|
+
* kind `"not-initialised"` carrying the line to run.
|
|
1620
|
+
* - A failure says which kind it is, so the observation can fall back by
|
|
1621
|
+
* depth ({@link classifyPlanReadFailure}).
|
|
1622
|
+
*
|
|
1623
|
+
* Uses the longInfra profile, like {@link terraformPlan}: a plan refreshes
|
|
1624
|
+
* every resource against its provider.
|
|
1625
|
+
*/
|
|
1626
|
+
export async function terraformPlanRead(
|
|
1627
|
+
args: TerraformRootArgs,
|
|
1628
|
+
signal?: AbortSignal,
|
|
1629
|
+
): Promise<TerraformPlanReadResult> {
|
|
1630
|
+
const { binary, root, dir } = await resolveRoot(args, signal);
|
|
1631
|
+
const env = terraformEnvironment(root);
|
|
1632
|
+
const initLine = terraformInitLine({ binary, dir, ...(root.backendConfig ? { backendConfig: root.backendConfig } : {}) });
|
|
1633
|
+
const notInitialised = (detail: string): TerraformPlanReadError =>
|
|
1634
|
+
new TerraformPlanReadError(
|
|
1635
|
+
"not-initialised",
|
|
1636
|
+
`terraform.roots.${args.root} (${dir}) ${detail}: run \`${initLine}\` first. chant never runs init on a read, since init writes into the root.`,
|
|
1637
|
+
dir,
|
|
1638
|
+
initLine,
|
|
1639
|
+
);
|
|
1640
|
+
|
|
1641
|
+
if (!rootIsInitialised(dir, { ...process.env, ...env })) throw notInitialised("has never been initialised");
|
|
1642
|
+
|
|
1643
|
+
const planDir = mkdtempSync(join(tmpdir(), PLAN_READ_DIR_PREFIX));
|
|
1644
|
+
const planFile = join(planDir, PLAN_READ_FILE);
|
|
1645
|
+
const command = terraformPlanReadCommand({ binary, planFile, ...(root.varFiles ? { varFiles: root.varFiles } : {}) });
|
|
1646
|
+
try {
|
|
1647
|
+
try {
|
|
1648
|
+
const { stderr } = await run(command, dir, env, signal);
|
|
1649
|
+
// The plan's own stdout is the human render, which this read never
|
|
1650
|
+
// uses; stderr carries warnings worth a human's eye.
|
|
1651
|
+
if (stderr) console.error(stderr);
|
|
1652
|
+
} catch (err) {
|
|
1653
|
+
const failure = err as ExecFailure;
|
|
1654
|
+
if (typeof failure.code !== "number") throw err;
|
|
1655
|
+
const output = `${failure.stderr ?? ""}\n${failure.stdout ?? ""}`.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1656
|
+
const kind = classifyPlanReadFailure(output);
|
|
1657
|
+
const line =
|
|
1658
|
+
output
|
|
1659
|
+
.split("\n")
|
|
1660
|
+
.map((l) => l.replace(/^[\s│╷╵]+/, "").trim())
|
|
1661
|
+
.find((l) => /^Error:/.test(l)) ?? `exit ${failure.code}`;
|
|
1662
|
+
if (kind === "not-initialised") throw notInitialised(`needs init (${line})`);
|
|
1663
|
+
throw new TerraformPlanReadError(kind, `${binary} plan failed in ${dir}: ${line}`, dir);
|
|
1664
|
+
}
|
|
1665
|
+
const shown = await run(terraformShowCommand({ binary, json: true, planFile }), dir, env, signal);
|
|
1666
|
+
return { json: JSON.parse(shown.stdout) as unknown, dir, planDir, command };
|
|
1667
|
+
} finally {
|
|
1668
|
+
rmSync(planDir, { recursive: true, force: true });
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1487
1672
|
/**
|
|
1488
1673
|
* The estate a choudoufu-only activity runs against, in precedence order:
|
|
1489
1674
|
* `args.estate` when the step names one, then the root's own `estate`
|
|
@@ -1560,8 +1745,12 @@ export interface ChoudoufuLivePlanDocumentResult extends LivePlanUnownedCounts {
|
|
|
1560
1745
|
json: unknown;
|
|
1561
1746
|
/** Absolute path of the root module directory. */
|
|
1562
1747
|
dir: string;
|
|
1563
|
-
/**
|
|
1564
|
-
|
|
1748
|
+
/**
|
|
1749
|
+
* Where the JSON document was written, relative to `dir`. Present only when
|
|
1750
|
+
* the caller asked for a file with `documentPath`; a read writes nothing
|
|
1751
|
+
* into the root (#4379).
|
|
1752
|
+
*/
|
|
1753
|
+
documentPath?: string;
|
|
1565
1754
|
/** The estate this plan ran against. */
|
|
1566
1755
|
estate: string;
|
|
1567
1756
|
}
|
|
@@ -1582,7 +1771,9 @@ export interface ChoudoufuLivePlanDocumentResult extends LivePlanUnownedCounts {
|
|
|
1582
1771
|
* the root's environment. The graph command runs in such a scope, so its
|
|
1583
1772
|
* observation and its prediction see one `live-plan` between them, and the
|
|
1584
1773
|
* graph's statuses and the prediction's figures are statements about the
|
|
1585
|
-
* same read. The document
|
|
1774
|
+
* same read. The document is returned, not written: the root is left
|
|
1775
|
+
* byte-for-byte as it was (#4379). A caller that wants the file passes
|
|
1776
|
+
* `documentPath` (relative to the root) and it is written there on request.
|
|
1586
1777
|
*/
|
|
1587
1778
|
export async function choudoufuLivePlanDocument(
|
|
1588
1779
|
args: ChoudoufuLivePlanArgs & { documentPath?: string },
|
|
@@ -1592,7 +1783,7 @@ export async function choudoufuLivePlanDocument(
|
|
|
1592
1783
|
const { binary, dir } = resolved;
|
|
1593
1784
|
const estate = resolveEstate(args, resolved, "choudoufuLivePlan");
|
|
1594
1785
|
const env = terraformEnvironment(resolved.root);
|
|
1595
|
-
const documentPath = args.documentPath
|
|
1786
|
+
const documentPath = args.documentPath;
|
|
1596
1787
|
// A root that declares its own estate is run without `-estate`: choudoufu
|
|
1597
1788
|
// refuses the flag beside a declaration and, since v0.14.0, settles the name
|
|
1598
1789
|
// from the declaration itself. See {@link choudoufuLivePlanCommand}.
|
|
@@ -1634,15 +1825,28 @@ export async function choudoufuLivePlanDocument(
|
|
|
1634
1825
|
// the identity on a supported binary. See {@link liveDocumentFrom}.
|
|
1635
1826
|
const document = liveDocumentFrom(jsonStdout);
|
|
1636
1827
|
const json: unknown = JSON.parse(document);
|
|
1637
|
-
writeFileSync(join(dir, documentPath), document);
|
|
1638
|
-
return {
|
|
1828
|
+
if (documentPath !== undefined) writeFileSync(join(dir, documentPath), document);
|
|
1829
|
+
return {
|
|
1830
|
+
drift,
|
|
1831
|
+
json,
|
|
1832
|
+
dir,
|
|
1833
|
+
...(documentPath === undefined ? {} : { documentPath }),
|
|
1834
|
+
estate,
|
|
1835
|
+
...countLivePlanUnowned(json),
|
|
1836
|
+
};
|
|
1639
1837
|
}
|
|
1640
1838
|
|
|
1641
1839
|
export async function choudoufuLivePlan(
|
|
1642
1840
|
args: ChoudoufuLivePlanArgs & { documentPath?: string },
|
|
1643
1841
|
signal?: AbortSignal,
|
|
1644
1842
|
): Promise<ChoudoufuLivePlanResult> {
|
|
1645
|
-
|
|
1843
|
+
// The Op activity is the one caller that wants the file: `documentPath` is a
|
|
1844
|
+
// declared `.out` ref a later step or reviewer opens (#2103), so it asks.
|
|
1845
|
+
const documentPath = args.documentPath ?? DEFAULT_LIVE_PLAN_DOCUMENT_FILE;
|
|
1846
|
+
const { drift, json, dir, estate, ...unowned } = await choudoufuLivePlanDocument(
|
|
1847
|
+
{ ...args, documentPath },
|
|
1848
|
+
signal,
|
|
1849
|
+
);
|
|
1646
1850
|
const resolved = await resolveRoot(args, signal);
|
|
1647
1851
|
const { binary } = resolved;
|
|
1648
1852
|
const env = terraformEnvironment(resolved.root);
|