@intentius/chant-lexicon-terraform 0.124.58 → 0.124.60
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/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/capability-plugin.js +3 -0
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/plan-digest.d.ts +18 -0
- package/dist/plan-digest.d.ts.map +1 -1
- package/dist/plan-digest.js +43 -1
- package/package.json +2 -2
- package/src/components/capability-plugin.test.ts +1 -0
- package/src/components/capability-plugin.ts +3 -0
- package/src/plan-digest.test.ts +89 -0
- package/src/plan-digest.ts +42 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capability-plugin.d.ts","sourceRoot":"","sources":["../../src/components/capability-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4HG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAiC,cAAc,EAAE,MAAM,wCAAwC,CAAC;AAExH,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"capability-plugin.d.ts","sourceRoot":"","sources":["../../src/components/capability-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4HG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAiC,cAAc,EAAE,MAAM,wCAAwC,CAAC;AAExH,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,2CAA2C,CAAC;AAInD,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,+CAA+C,CAAC;AACzG,OAAO,KAAK,EAAmB,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,kDAAkD,CAAC;AA0BlI,OAAO,EAA0D,KAAK,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAIjH,0CAA0C;AAC1C,MAAM,WAAW,uBAAuB;IACtC,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,gFAAgF;IAChF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+HAA+H;IAC/H,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yGAAyG;IACzG,WAAW,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC7B,qFAAqF;IACrF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gHAAgH;IAChH,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAC;CAC9B;AAED,6FAA6F;AAC7F,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,YAAY,EAAE,OAAO,CAAC;IACtB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAyGD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,CAerE;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG,oBAAoB,CAEhI;AAID;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,kBAInC,CAAC;AA4BF,4GAA4G;AAC5G,eAAO,MAAM,iBAAiB,0BAA0B,CAAC;AAEzD,iHAAiH;AACjH,MAAM,WAAW,sBAAuB,SAAQ,iBAAiB;IAC/D,IAAI,EAAE,uBAAuB,CAAC;CAC/B;AAsFD;;;;;;;GAOG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;aAEpB,IAAI,EAAE,MAAM;IADxC,QAAQ,CAAC,SAAS,QAAQ;IAC1B,YAA4B,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAGxD;CACF;AA6DD,eAAO,MAAM,wBAAwB,EAAE,UAAU,CAAC,uBAAuB,EAAE,wBAAwB,CA0HlG,CAAC;AAEF,eAAO,MAAM,uBAAuB;aAClC,KAAK,YAAG,iBAAiB;aAEzB,KAAK,YAAG,wBAAwB,EAAE,sBAAsB;aACxD,IAAI,YAAG,gBAAgB;aAEvB,MAAM,YAAG,kBAAkB;CACnB,CAAC;AAEX,eAAO,MAAM,yBAAyB,EAAE,gBAcvC,CAAC"}
|
|
@@ -128,6 +128,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
128
128
|
import { loadChantConfigUpward } from "@intentius/chant/config";
|
|
129
129
|
import { PlanShapeMovedError, planShapeDifferences, planShapeDigest, } from "@intentius/chant/components/covered-round";
|
|
130
130
|
import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
|
|
131
|
+
import { terraformResourceDigests } from "../plan-digest.js";
|
|
131
132
|
import { ownPackageVersion } from "@intentius/chant/components/capability-plugin";
|
|
132
133
|
import { choudoufuMemberOutputs, choudoufuMemberPlanSet, choudoufuPlanSet, choudoufuWaveApply, choudoufuWaveRoot, terraformApply, terraformInit, terraformOutputs, terraformPlan, } from "../op/activities/terraform.js";
|
|
133
134
|
import { terraformPlanMoved } from "../exit-codes.js";
|
|
@@ -501,6 +502,8 @@ export const terraformApplyCapability = {
|
|
|
501
502
|
planDigest: planned.planDigest,
|
|
502
503
|
artifact: { ...planned, ...(lockRows ? { lockRows } : {}) },
|
|
503
504
|
...(changeSet ? { changeSet } : {}),
|
|
505
|
+
// plywood#55: each changed resource's own digest, which a merged wave compares with the pull request's per resource.
|
|
506
|
+
resourceDigests: terraformResourceDigests(json),
|
|
504
507
|
outputs: terraformPlannedOutputs(json),
|
|
505
508
|
shape: terraformPlanShape(json, input.root),
|
|
506
509
|
...(targets ? { targets } : {}),
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "22c97c3a49ae63d8af9db4b7dff40c394032058689df59ec9388d9b9100c21a8",
|
|
5
5
|
"meta.json": "ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356",
|
|
6
6
|
"types/index.d.ts": "022927e6a7b351cc4cf52d5f973f60ccd140e5810e17b32fb8868965aa1db161",
|
|
7
7
|
"rules/plan-before-apply.ts": "7c10245b2c3335253b48ee7d8b075d89f43116bde20313a5d1031b343fef1282",
|
|
@@ -59,5 +59,5 @@
|
|
|
59
59
|
"rules/tf047.ts": "bfa86f85c4c5efff297280fc2f15104d6319fa96dfc444a31829fb7353224092",
|
|
60
60
|
"skills/chant-terraform.md": "53dd11c7b289652f92fdaf2c03766abda46d8964e31fe709e8b142ced4f19c50"
|
|
61
61
|
},
|
|
62
|
-
"composite": "
|
|
62
|
+
"composite": "3fca866c82436936752ced650ed0b4ae5679ea8953d0f7aa88c7bfa3827164b8"
|
|
63
63
|
}
|
package/dist/manifest.json
CHANGED
package/dist/plan-digest.d.ts
CHANGED
|
@@ -25,6 +25,15 @@ export interface TerraformPlannedChange {
|
|
|
25
25
|
replace_paths: unknown;
|
|
26
26
|
importing: unknown;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* `after` without the values the plan says are known only after apply
|
|
30
|
+
* (plywood#56). terraform and tofu leave such a value out of `after` and mark
|
|
31
|
+
* it in `after_unknown`; a renderer that prints a stand-in instead (a null, a
|
|
32
|
+
* placeholder string, a value drawn while planning) would otherwise make two
|
|
33
|
+
* plans of one create differ. The mark itself stays in the digest, so a
|
|
34
|
+
* value turning from known to unknown still changes it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function withoutUnknown(after: unknown, unknown: unknown): unknown;
|
|
28
37
|
/**
|
|
29
38
|
* The change set a `terraform show -json` plan describes, canonicalised
|
|
30
39
|
* (#2300): every proposed change, at its address, with the values that would
|
|
@@ -56,4 +65,13 @@ export declare function terraformPlanChangeSet(planJson: unknown): {
|
|
|
56
65
|
* is the same plan both times.
|
|
57
66
|
*/
|
|
58
67
|
export declare function terraformPlanDigest(planJson: unknown): string;
|
|
68
|
+
/**
|
|
69
|
+
* The digest of each resource the plan changes, by address (plywood#55): its
|
|
70
|
+
* change as {@link terraformPlanChangeSet} projects it, a deposed object apart
|
|
71
|
+
* from the live one, and each action invocation by its address. A no-op or
|
|
72
|
+
* read entry is not in it, and neither is an output, so what a pull request
|
|
73
|
+
* left alone never makes its plan differ from one made after another change
|
|
74
|
+
* landed on it.
|
|
75
|
+
*/
|
|
76
|
+
export declare function terraformResourceDigests(planJson: unknown): Record<string, string>;
|
|
59
77
|
//# sourceMappingURL=plan-digest.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan-digest.d.ts","sourceRoot":"","sources":["../src/plan-digest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,OAAO,GAAG;IACzD,eAAe,EAAE,sBAAsB,EAAE,CAAC;IAC1C,aAAa,EAAE,OAAO,CAAC;IACvB,iBAAiB,CAAC,EAAE,OAAO,EAAE,CAAC;CAC/B,CAkCA;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAE7D"}
|
|
1
|
+
{"version":3,"file":"plan-digest.d.ts","sourceRoot":"","sources":["../src/plan-digest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,OAAO,CAAC;IACvB,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAQxE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,OAAO,GAAG;IACzD,eAAe,EAAE,sBAAsB,EAAE,CAAC;IAC1C,aAAa,EAAE,OAAO,CAAC;IACvB,iBAAiB,CAAC,EAAE,OAAO,EAAE,CAAC;CAC/B,CAkCA;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAE7D;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAalF"}
|
package/dist/plan-digest.js
CHANGED
|
@@ -6,6 +6,25 @@
|
|
|
6
6
|
* what lets terragucci bundle it into one file (#3421).
|
|
7
7
|
*/
|
|
8
8
|
import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
|
|
9
|
+
/**
|
|
10
|
+
* `after` without the values the plan says are known only after apply
|
|
11
|
+
* (plywood#56). terraform and tofu leave such a value out of `after` and mark
|
|
12
|
+
* it in `after_unknown`; a renderer that prints a stand-in instead (a null, a
|
|
13
|
+
* placeholder string, a value drawn while planning) would otherwise make two
|
|
14
|
+
* plans of one create differ. The mark itself stays in the digest, so a
|
|
15
|
+
* value turning from known to unknown still changes it.
|
|
16
|
+
*/
|
|
17
|
+
export function withoutUnknown(after, unknown) {
|
|
18
|
+
if (unknown === true)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (Array.isArray(after) && Array.isArray(unknown))
|
|
21
|
+
return after.map((v, i) => (unknown[i] === true ? null : withoutUnknown(v, unknown[i])));
|
|
22
|
+
if (after !== null && typeof after === "object" && !Array.isArray(after) && unknown !== null && typeof unknown === "object" && !Array.isArray(unknown)) {
|
|
23
|
+
const marks = unknown;
|
|
24
|
+
return Object.fromEntries(Object.entries(after).filter(([k]) => marks[k] !== true).map(([k, v]) => [k, withoutUnknown(v, marks[k])]));
|
|
25
|
+
}
|
|
26
|
+
return after;
|
|
27
|
+
}
|
|
9
28
|
/**
|
|
10
29
|
* The change set a `terraform show -json` plan describes, canonicalised
|
|
11
30
|
* (#2300): every proposed change, at its address, with the values that would
|
|
@@ -37,7 +56,7 @@ export function terraformPlanChangeSet(planJson) {
|
|
|
37
56
|
deposed: e.deposed,
|
|
38
57
|
actions: change.actions,
|
|
39
58
|
before: change.before,
|
|
40
|
-
after: change.after,
|
|
59
|
+
after: withoutUnknown(change.after, change.after_unknown),
|
|
41
60
|
after_unknown: change.after_unknown,
|
|
42
61
|
replace_paths: change.replace_paths,
|
|
43
62
|
importing: change.importing,
|
|
@@ -65,3 +84,26 @@ export function terraformPlanChangeSet(planJson) {
|
|
|
65
84
|
export function terraformPlanDigest(planJson) {
|
|
66
85
|
return computePlanDigest("terraform-plan", terraformPlanChangeSet(planJson));
|
|
67
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The digest of each resource the plan changes, by address (plywood#55): its
|
|
89
|
+
* change as {@link terraformPlanChangeSet} projects it, a deposed object apart
|
|
90
|
+
* from the live one, and each action invocation by its address. A no-op or
|
|
91
|
+
* read entry is not in it, and neither is an output, so what a pull request
|
|
92
|
+
* left alone never makes its plan differ from one made after another change
|
|
93
|
+
* landed on it.
|
|
94
|
+
*/
|
|
95
|
+
export function terraformResourceDigests(planJson) {
|
|
96
|
+
const { resourceChanges, actionInvocations } = terraformPlanChangeSet(planJson);
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const c of resourceChanges) {
|
|
99
|
+
const actions = Array.isArray(c.actions) ? c.actions : [];
|
|
100
|
+
if (actions.length === 0 || actions.every((a) => a === "no-op" || a === "read"))
|
|
101
|
+
continue;
|
|
102
|
+
out[`${String(c.address)}${c.deposed ? `#deposed:${String(c.deposed)}` : ""}`] = computePlanDigest("terraform-resource", c);
|
|
103
|
+
}
|
|
104
|
+
for (const a of actionInvocations ?? []) {
|
|
105
|
+
const address = a?.address;
|
|
106
|
+
out[`action:${typeof address === "string" ? address : JSON.stringify(a)}`] = computePlanDigest("terraform-action", a);
|
|
107
|
+
}
|
|
108
|
+
return Object.fromEntries(Object.entries(out).sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0)));
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-terraform",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.60",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src/",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"access": "public"
|
|
94
94
|
},
|
|
95
95
|
"peerDependencies": {
|
|
96
|
-
"@intentius/chant": "^0.124.
|
|
96
|
+
"@intentius/chant": "^0.124.60",
|
|
97
97
|
"zod": "^4.3.6"
|
|
98
98
|
},
|
|
99
99
|
"description": "terraform lexicon for chant, existing Terraform root modules as declarable entities",
|
|
@@ -217,6 +217,7 @@ describe("terraform-apply", () => {
|
|
|
217
217
|
member: "app",
|
|
218
218
|
planDigest: "jcs1-sha256:app",
|
|
219
219
|
artifact: { planFile: "chant.tfplan", planDigest: "jcs1-sha256:app" },
|
|
220
|
+
resourceDigests: {},
|
|
220
221
|
outputs: { known: {}, unknown: [] },
|
|
221
222
|
shape: { member: "app", entries: [], unknownInputs: [] },
|
|
222
223
|
});
|
|
@@ -136,6 +136,7 @@ import {
|
|
|
136
136
|
} from "@intentius/chant/components/covered-round";
|
|
137
137
|
import type { ChangeSetPart } from "@intentius/chant/change-set";
|
|
138
138
|
import { computePlanDigest } from "@intentius/chant/lifecycle/plan-digest";
|
|
139
|
+
import { terraformResourceDigests } from "../plan-digest";
|
|
139
140
|
import { ownPackageVersion, type CapabilityPlugin } from "@intentius/chant/components/capability-plugin";
|
|
140
141
|
import type { EngineMemberRef, EngineOutputSource, EnginePlannedOutputs } from "@intentius/chant/components/verbs/member-outputs";
|
|
141
142
|
import {
|
|
@@ -580,6 +581,8 @@ export const terraformApplyCapability: Capability<TerraformApplyStepInput, Terra
|
|
|
580
581
|
planDigest: planned.planDigest,
|
|
581
582
|
artifact: { ...planned, ...(lockRows ? { lockRows } : {}) },
|
|
582
583
|
...(changeSet ? { changeSet } : {}),
|
|
584
|
+
// plywood#55: each changed resource's own digest, which a merged wave compares with the pull request's per resource.
|
|
585
|
+
resourceDigests: terraformResourceDigests(json),
|
|
583
586
|
outputs: terraformPlannedOutputs(json),
|
|
584
587
|
shape: terraformPlanShape(json, input.root),
|
|
585
588
|
...(targets ? { targets } : {}),
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a terraform or tofu plan's digest leaves out (plywood#56): the values
|
|
3
|
+
* the plan says are known only after apply. A create of a resource with a
|
|
4
|
+
* generated name (`name_prefix`, a random id, a computed output) has to give
|
|
5
|
+
* the same digest in every plan of the same change.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { terraformPlanDigest, terraformResourceDigests, withoutUnknown } from "./plan-digest";
|
|
9
|
+
|
|
10
|
+
function plan(after: Record<string, unknown>, unknown: Record<string, unknown> = { id: true, name: true }) {
|
|
11
|
+
return {
|
|
12
|
+
format_version: "1.2",
|
|
13
|
+
terraform_version: "1.9.0",
|
|
14
|
+
timestamp: "2026-10-31T00:00:00Z",
|
|
15
|
+
resource_changes: [
|
|
16
|
+
{ address: "aws_sqs_queue.worker", mode: "managed", type: "aws_sqs_queue", name: "worker", provider_name: "registry.opentofu.org/hashicorp/aws", change: { actions: ["create"], before: null, after, after_unknown: unknown } },
|
|
17
|
+
{ address: "random_id.suffix", mode: "managed", type: "random_id", name: "suffix", provider_name: "registry.opentofu.org/hashicorp/random", change: { actions: ["create"], before: null, after: { byte_length: 4, keepers: null }, after_unknown: { id: true, hex: true } } },
|
|
18
|
+
],
|
|
19
|
+
output_changes: {},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("a create's digest leaves out values known only after apply", () => {
|
|
24
|
+
test("the same create planned twice has one digest, whatever stood in for the unknowns", () => {
|
|
25
|
+
const first = plan({ visibility_timeout_seconds: 30, tags: { team: "a" } });
|
|
26
|
+
const second = plan({ visibility_timeout_seconds: 30, tags: { team: "a" }, name: "worker-20261031120000", id: "placeholder-1" });
|
|
27
|
+
const third = plan({ visibility_timeout_seconds: 30, tags: { team: "a" }, name: null, id: null });
|
|
28
|
+
expect(terraformPlanDigest(second)).toBe(terraformPlanDigest(first));
|
|
29
|
+
expect(terraformPlanDigest(third)).toBe(terraformPlanDigest(first));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("a changed input a reviewer saw still changes it", () => {
|
|
33
|
+
const base = plan({ visibility_timeout_seconds: 30 });
|
|
34
|
+
expect(terraformPlanDigest(plan({ visibility_timeout_seconds: 60 }))).not.toBe(terraformPlanDigest(base));
|
|
35
|
+
expect(terraformPlanDigest(plan({ visibility_timeout_seconds: 30, delay_seconds: 5 }))).not.toBe(terraformPlanDigest(base));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("a value turning from known to unknown changes it", () => {
|
|
39
|
+
const known = plan({ visibility_timeout_seconds: 30, name: "worker" }, { id: true });
|
|
40
|
+
const unknown = plan({ visibility_timeout_seconds: 30 }, { id: true, name: true });
|
|
41
|
+
expect(terraformPlanDigest(known)).not.toBe(terraformPlanDigest(unknown));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("nested and listed unknowns are left out, known neighbours are kept", () => {
|
|
45
|
+
expect(withoutUnknown({ a: { b: "x", c: "gen" }, l: ["p", "gen", "q"] }, { a: { c: true }, l: [false, true, false] })).toEqual({ a: { b: "x" }, l: ["p", null, "q"] });
|
|
46
|
+
expect(withoutUnknown({ a: 1 }, {})).toEqual({ a: 1 });
|
|
47
|
+
expect(withoutUnknown(null, { id: true })).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("the digest of each changed resource (plywood#55)", () => {
|
|
52
|
+
const two = (queueAfter: Record<string, unknown>, bucketActions: string[]) => ({
|
|
53
|
+
resource_changes: [
|
|
54
|
+
{ address: "aws_sqs_queue.worker", type: "aws_sqs_queue", name: "worker", change: { actions: ["create"], before: null, after: queueAfter, after_unknown: { id: true, name: true } } },
|
|
55
|
+
{ address: "aws_s3_bucket.logs", type: "aws_s3_bucket", name: "logs", change: { actions: bucketActions, before: { acl: "private" }, after: { acl: bucketActions[0] === "update" ? "public-read" : "private" }, after_unknown: {} } },
|
|
56
|
+
],
|
|
57
|
+
action_invocations: [{ address: "action.aws_lambda_invoke.warm", type: "aws_lambda_invoke" }],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("a no-op is not in it, and an create of unknown values has one digest across plans", () => {
|
|
61
|
+
const a = terraformResourceDigests(two({ delay_seconds: 0 }, ["no-op"]));
|
|
62
|
+
const b = terraformResourceDigests(two({ delay_seconds: 0, name: "placeholder", id: "x" }, ["no-op"]));
|
|
63
|
+
expect(Object.keys(a)).toEqual(["action:action.aws_lambda_invoke.warm", "aws_sqs_queue.worker"]);
|
|
64
|
+
expect(b).toEqual(a);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("a change to another resource leaves this one's digest as it was", () => {
|
|
68
|
+
const quiet = terraformResourceDigests(two({ delay_seconds: 0 }, ["no-op"]));
|
|
69
|
+
const busy = terraformResourceDigests(two({ delay_seconds: 0 }, ["update"]));
|
|
70
|
+
expect(busy["aws_sqs_queue.worker"]).toBe(quiet["aws_sqs_queue.worker"]);
|
|
71
|
+
expect(Object.keys(busy)).toContain("aws_s3_bucket.logs");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("a reviewed input changes it", () => {
|
|
75
|
+
const a = terraformResourceDigests(two({ delay_seconds: 0 }, ["no-op"]));
|
|
76
|
+
const b = terraformResourceDigests(two({ delay_seconds: 9 }, ["no-op"]));
|
|
77
|
+
expect(b["aws_sqs_queue.worker"]).not.toBe(a["aws_sqs_queue.worker"]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("a deposed object is kept apart from the live one", () => {
|
|
81
|
+
const doc = {
|
|
82
|
+
resource_changes: [
|
|
83
|
+
{ address: "aws_x.a", change: { actions: ["create"], before: null, after: {}, after_unknown: {} } },
|
|
84
|
+
{ address: "aws_x.a", deposed: "abc", change: { actions: ["delete"], before: {}, after: null, after_unknown: {} } },
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
expect(Object.keys(terraformResourceDigests(doc))).toEqual(["aws_x.a", "aws_x.a#deposed:abc"]);
|
|
88
|
+
});
|
|
89
|
+
});
|
package/src/plan-digest.ts
CHANGED
|
@@ -29,6 +29,24 @@ export interface TerraformPlannedChange {
|
|
|
29
29
|
importing: unknown;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* `after` without the values the plan says are known only after apply
|
|
34
|
+
* (plywood#56). terraform and tofu leave such a value out of `after` and mark
|
|
35
|
+
* it in `after_unknown`; a renderer that prints a stand-in instead (a null, a
|
|
36
|
+
* placeholder string, a value drawn while planning) would otherwise make two
|
|
37
|
+
* plans of one create differ. The mark itself stays in the digest, so a
|
|
38
|
+
* value turning from known to unknown still changes it.
|
|
39
|
+
*/
|
|
40
|
+
export function withoutUnknown(after: unknown, unknown: unknown): unknown {
|
|
41
|
+
if (unknown === true) return undefined;
|
|
42
|
+
if (Array.isArray(after) && Array.isArray(unknown)) return after.map((v, i) => (unknown[i] === true ? null : withoutUnknown(v, unknown[i])));
|
|
43
|
+
if (after !== null && typeof after === "object" && !Array.isArray(after) && unknown !== null && typeof unknown === "object" && !Array.isArray(unknown)) {
|
|
44
|
+
const marks = unknown as Record<string, unknown>;
|
|
45
|
+
return Object.fromEntries(Object.entries(after as Record<string, unknown>).filter(([k]) => marks[k] !== true).map(([k, v]) => [k, withoutUnknown(v, marks[k])]));
|
|
46
|
+
}
|
|
47
|
+
return after;
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
/**
|
|
33
51
|
* The change set a `terraform show -json` plan describes, canonicalised
|
|
34
52
|
* (#2300): every proposed change, at its address, with the values that would
|
|
@@ -64,7 +82,7 @@ export function terraformPlanChangeSet(planJson: unknown): {
|
|
|
64
82
|
deposed: e.deposed,
|
|
65
83
|
actions: change.actions,
|
|
66
84
|
before: change.before,
|
|
67
|
-
after: change.after,
|
|
85
|
+
after: withoutUnknown(change.after, change.after_unknown),
|
|
68
86
|
after_unknown: change.after_unknown,
|
|
69
87
|
replace_paths: change.replace_paths,
|
|
70
88
|
importing: change.importing,
|
|
@@ -97,3 +115,26 @@ export function terraformPlanChangeSet(planJson: unknown): {
|
|
|
97
115
|
export function terraformPlanDigest(planJson: unknown): string {
|
|
98
116
|
return computePlanDigest("terraform-plan", terraformPlanChangeSet(planJson));
|
|
99
117
|
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The digest of each resource the plan changes, by address (plywood#55): its
|
|
121
|
+
* change as {@link terraformPlanChangeSet} projects it, a deposed object apart
|
|
122
|
+
* from the live one, and each action invocation by its address. A no-op or
|
|
123
|
+
* read entry is not in it, and neither is an output, so what a pull request
|
|
124
|
+
* left alone never makes its plan differ from one made after another change
|
|
125
|
+
* landed on it.
|
|
126
|
+
*/
|
|
127
|
+
export function terraformResourceDigests(planJson: unknown): Record<string, string> {
|
|
128
|
+
const { resourceChanges, actionInvocations } = terraformPlanChangeSet(planJson);
|
|
129
|
+
const out: Record<string, string> = {};
|
|
130
|
+
for (const c of resourceChanges) {
|
|
131
|
+
const actions = Array.isArray(c.actions) ? c.actions : [];
|
|
132
|
+
if (actions.length === 0 || actions.every((a) => a === "no-op" || a === "read")) continue;
|
|
133
|
+
out[`${String(c.address)}${c.deposed ? `#deposed:${String(c.deposed)}` : ""}`] = computePlanDigest("terraform-resource", c);
|
|
134
|
+
}
|
|
135
|
+
for (const a of actionInvocations ?? []) {
|
|
136
|
+
const address = (a as { address?: unknown } | null)?.address;
|
|
137
|
+
out[`action:${typeof address === "string" ? address : JSON.stringify(a)}`] = computePlanDigest("terraform-action", a);
|
|
138
|
+
}
|
|
139
|
+
return Object.fromEntries(Object.entries(out).sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0)));
|
|
140
|
+
}
|