@intentius/chant-lexicon-aws 0.28.0 → 0.29.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/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/lifecycle-integration.test.ts +139 -5
- package/src/plugin.ts +31 -8
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "eac9b0bddda5b4b10352c04ef292ce7f707f72d7b9fe74a06426331963c8c8c8",
|
|
5
5
|
"meta.json": "8d6445b7ea5f7803c83cf49daab639b1df66388c1512f466b888b0ad57f60a9a",
|
|
6
6
|
"types/index.d.ts": "da899ff303a18a7fdb270570bce784703283d4081f806e69ac8c9dfd4bac7c50",
|
|
7
7
|
"rules/hardcoded-region.ts": "5a0eaf7ab391231fe6cd51426ece29539cb4b36f31c8dd060956638fed55722a",
|
|
@@ -59,5 +59,5 @@
|
|
|
59
59
|
"skills/chant-aws-eks.md": "8789255709ff004ad0a875fd5999edcdc66fc6e33d710db058d9f42703bcfdfe",
|
|
60
60
|
"skills/chant-aws-carve-terraform.md": "f4c6fe1c702250665f90d82b3bdaba5ea50ae75e380a8ae321dad6eb1c39683e"
|
|
61
61
|
},
|
|
62
|
-
"composite": "
|
|
62
|
+
"composite": "46d41420d193e652b241c1388259670efee2423e1a8026507cd1fd5b2d5175a4"
|
|
63
63
|
}
|
package/dist/manifest.json
CHANGED
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAkI,MAAM,0BAA0B,CAAC;AAwB9L;;sFAEsF;AACtF,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,aAyyBvB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-aws",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "AWS CloudFormation lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"typescript": "^5.9.3"
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|
|
83
|
-
"@intentius/chant": "^0.
|
|
83
|
+
"@intentius/chant": "^0.29.0",
|
|
84
84
|
"typescript": "^5.9.3"
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -27,6 +27,9 @@ vi.mock("@intentius/chant/runtime-adapter", async (importOriginal) => {
|
|
|
27
27
|
const { awsPlugin } = await import("./plugin");
|
|
28
28
|
const { liveImportFromPlugins } = await import("@intentius/chant/cli/commands/import");
|
|
29
29
|
const { buildChangeSet } = await import("@intentius/chant/lifecycle/change-set");
|
|
30
|
+
const { normalizeObservation } = await import("@intentius/chant/observation");
|
|
31
|
+
const { liveEvidenceFromChangeSet, reconcileStatus } = await import("@intentius/chant/lifecycle/status");
|
|
32
|
+
const { describeObservationConformance } = await import("@intentius/chant-test-utils");
|
|
30
33
|
|
|
31
34
|
const liveTemplate = {
|
|
32
35
|
AWSTemplateFormatVersion: "2010-09-09",
|
|
@@ -84,12 +87,18 @@ describe("aws lifecycle integration (#163)", () => {
|
|
|
84
87
|
return Promise.resolve(ok(JSON.stringify({ Stacks: [{ Outputs: [] }] })));
|
|
85
88
|
});
|
|
86
89
|
|
|
87
|
-
const observedNow =
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
const { resources: observedNow } = normalizeObservation(
|
|
91
|
+
await awsPlugin.describeResources!({
|
|
92
|
+
environment: "prod",
|
|
93
|
+
buildOutput: "",
|
|
94
|
+
entityNames: ["MyBucket"],
|
|
95
|
+
entities: new Map(),
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
92
98
|
expect(observedNow.MyBucket?.type).toBe("AWS::S3::Bucket");
|
|
99
|
+
// Ownership verdicts are total (#1089): describe-stack-resources carries no
|
|
100
|
+
// tags, so the verdict is an explicit `unknown`, not a missing field.
|
|
101
|
+
expect(observedNow.MyBucket?.ownership).toBe("unknown");
|
|
93
102
|
|
|
94
103
|
// Declared "MyQueue" is absent from live → create; live "MyBucket" is
|
|
95
104
|
// undeclared and unmarked → adopt (never delete without ownership).
|
|
@@ -144,4 +153,129 @@ describe("aws lifecycle integration (#163)", () => {
|
|
|
144
153
|
expect(obs).toBeNull();
|
|
145
154
|
});
|
|
146
155
|
});
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The #1089 chain on the real plugin: a CloudFormation read that fails for
|
|
159
|
+
* any reason other than "stack does not exist" reports every declared entity
|
|
160
|
+
* NOT-OBSERVED, and that survives describe → plan → component status.
|
|
161
|
+
*/
|
|
162
|
+
test("tri-state chain: a failed stack read stays unobserved through describe → plan → status (#1089)", async () => {
|
|
163
|
+
spawnMock.mockResolvedValue({ stdout: "", stderr: "Unable to locate credentials", exitCode: 255 });
|
|
164
|
+
|
|
165
|
+
const observed = normalizeObservation(
|
|
166
|
+
await awsPlugin.describeResources!({
|
|
167
|
+
environment: "prod",
|
|
168
|
+
buildOutput: "",
|
|
169
|
+
entityNames: ["MyBucket", "MyQueue"],
|
|
170
|
+
entities: new Map(),
|
|
171
|
+
}),
|
|
172
|
+
);
|
|
173
|
+
expect(observed.resources).toEqual({});
|
|
174
|
+
expect(observed.unobserved.MyBucket.reason).toBe("no-credentials");
|
|
175
|
+
|
|
176
|
+
const cs = buildChangeSet("prod", {
|
|
177
|
+
declared: new Set(["MyBucket", "MyQueue"]),
|
|
178
|
+
observedNow: observed.resources,
|
|
179
|
+
observedThen: undefined,
|
|
180
|
+
unobserved: observed.unobserved,
|
|
181
|
+
});
|
|
182
|
+
expect(cs.entries.map((e) => e.action)).toEqual(["unobserved", "unobserved"]);
|
|
183
|
+
|
|
184
|
+
const rows = reconcileStatus("prod", [
|
|
185
|
+
{ component: "MyBucket", env: "prod", digest: "sha256:abc", gitSha: "g", runId: "r", timestamp: "2026-01-01T00:00:00Z", actor: "ci" },
|
|
186
|
+
], { liveEvidence: liveEvidenceFromChangeSet(cs) });
|
|
187
|
+
expect(rows[0].reconciliation).toBe("unknown");
|
|
188
|
+
expect(rows[0].live).toBeUndefined();
|
|
189
|
+
expect(rows[0].unobserved?.reason).toBe("no-credentials");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("a stack that does not exist is a real absence — every declared entity is a create", async () => {
|
|
193
|
+
spawnMock.mockResolvedValue({ stdout: "", stderr: "ValidationError: Stack with id prod does not exist", exitCode: 255 });
|
|
194
|
+
|
|
195
|
+
const observed = normalizeObservation(
|
|
196
|
+
await awsPlugin.describeResources!({
|
|
197
|
+
environment: "prod",
|
|
198
|
+
buildOutput: "",
|
|
199
|
+
entityNames: ["MyBucket"],
|
|
200
|
+
entities: new Map(),
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
expect(observed.resources).toEqual({});
|
|
204
|
+
expect(observed.unobserved).toEqual({});
|
|
205
|
+
|
|
206
|
+
const cs = buildChangeSet("prod", {
|
|
207
|
+
declared: new Set(["MyBucket"]),
|
|
208
|
+
observedNow: observed.resources,
|
|
209
|
+
observedThen: undefined,
|
|
210
|
+
unobserved: observed.unobserved,
|
|
211
|
+
});
|
|
212
|
+
expect(cs.entries[0].action).toBe("create");
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// The shared conformance suite (#1089).
|
|
217
|
+
describeObservationConformance({
|
|
218
|
+
lexicon: "aws",
|
|
219
|
+
scenarios: [
|
|
220
|
+
{
|
|
221
|
+
name: "a stack read that fails on credentials",
|
|
222
|
+
declared: ["MyBucket", "MyQueue"],
|
|
223
|
+
expectUnobserved: ["MyBucket", "MyQueue"],
|
|
224
|
+
run: () => {
|
|
225
|
+
spawnMock.mockResolvedValue({ stdout: "", stderr: "Unable to locate credentials", exitCode: 255 });
|
|
226
|
+
return awsPlugin.describeResources!({
|
|
227
|
+
environment: "prod",
|
|
228
|
+
buildOutput: "",
|
|
229
|
+
entityNames: ["MyBucket", "MyQueue"],
|
|
230
|
+
entities: new Map(),
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "a stack that does not exist yet",
|
|
236
|
+
declared: ["MyBucket"],
|
|
237
|
+
expectAbsent: ["MyBucket"],
|
|
238
|
+
run: () => {
|
|
239
|
+
spawnMock.mockResolvedValue({ stdout: "", stderr: "ValidationError: Stack with id prod does not exist", exitCode: 255 });
|
|
240
|
+
return awsPlugin.describeResources!({
|
|
241
|
+
environment: "prod",
|
|
242
|
+
buildOutput: "",
|
|
243
|
+
entityNames: ["MyBucket"],
|
|
244
|
+
entities: new Map(),
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: "a healthy stack read",
|
|
250
|
+
declared: ["MyBucket"],
|
|
251
|
+
expectPresent: ["MyBucket"],
|
|
252
|
+
run: () => {
|
|
253
|
+
spawnMock.mockImplementation((argv?: string[]) =>
|
|
254
|
+
Promise.resolve(
|
|
255
|
+
argv?.includes("describe-stack-resources")
|
|
256
|
+
? ok(
|
|
257
|
+
JSON.stringify({
|
|
258
|
+
StackResources: [
|
|
259
|
+
{
|
|
260
|
+
LogicalResourceId: "MyBucket",
|
|
261
|
+
ResourceType: "AWS::S3::Bucket",
|
|
262
|
+
PhysicalResourceId: "my-bucket",
|
|
263
|
+
ResourceStatus: "CREATE_COMPLETE",
|
|
264
|
+
Timestamp: "2026-01-01T00:00:00Z",
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
}),
|
|
268
|
+
)
|
|
269
|
+
: ok(JSON.stringify({ Stacks: [{ Outputs: [] }] })),
|
|
270
|
+
),
|
|
271
|
+
);
|
|
272
|
+
return awsPlugin.describeResources!({
|
|
273
|
+
environment: "prod",
|
|
274
|
+
buildOutput: "",
|
|
275
|
+
entityNames: ["MyBucket"],
|
|
276
|
+
entities: new Map(),
|
|
277
|
+
});
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
],
|
|
147
281
|
});
|
package/src/plugin.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
2
|
import { detectTemplate } from "./detect";
|
|
3
|
-
import type { LexiconPlugin, IntrinsicDef, ResourceMetadata, ExportedTemplate, ResourceSelector, InitTemplateSet, StackStatusObservation } from "@intentius/chant/lexicon";
|
|
3
|
+
import type { LexiconPlugin, IntrinsicDef, ObservationResult, ResourceMetadata, ExportedTemplate, ResourceSelector, InitTemplateSet, StackStatusObservation } from "@intentius/chant/lexicon";
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
import type { LintRule } from "@intentius/chant/lint/rule";
|
|
6
6
|
import type { TemplateParser } from "@intentius/chant/import/parser";
|
|
@@ -534,8 +534,9 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
534
534
|
entityNames: string[];
|
|
535
535
|
stack?: string;
|
|
536
536
|
owned?: boolean;
|
|
537
|
-
}): Promise<
|
|
537
|
+
}): Promise<ObservationResult> {
|
|
538
538
|
const { getRuntime } = await import("@intentius/chant/runtime-adapter");
|
|
539
|
+
const { observation, unobservedAll } = await import("@intentius/chant/observation");
|
|
539
540
|
const rt = getRuntime();
|
|
540
541
|
const resources: Record<string, ResourceMetadata> = {};
|
|
541
542
|
|
|
@@ -544,7 +545,7 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
544
545
|
// determined here. Degrade to detect-only rather than silently filtering.
|
|
545
546
|
// eslint-disable-next-line no-console
|
|
546
547
|
console.warn(
|
|
547
|
-
"[aws] ownership filter unavailable on describeResources (no tags from describe-stack-resources) — returning all; use `chant import --from <env> --owned` for ownership-filtered export",
|
|
548
|
+
"[aws] ownership filter unavailable on describeResources (no tags from describe-stack-resources) — returning all, each with an explicit `unknown` verdict; use `chant import --from <env> --owned` for ownership-filtered export",
|
|
548
549
|
);
|
|
549
550
|
}
|
|
550
551
|
|
|
@@ -565,12 +566,26 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
565
566
|
if (listResult.exitCode !== 0) {
|
|
566
567
|
// A stack that doesn't exist yet is the pre-first-apply state: nothing is
|
|
567
568
|
// deployed for this env, so there are no live resources (every declared
|
|
568
|
-
// resource is "pending") — not an error.
|
|
569
|
-
//
|
|
569
|
+
// resource is "pending") — not an error. That is a real absence, so the
|
|
570
|
+
// empty result is the honest one and `create` is the right proposal.
|
|
570
571
|
if (stackDoesNotExist(listResult.stderr)) {
|
|
571
|
-
return resources;
|
|
572
|
+
return observation(resources);
|
|
572
573
|
}
|
|
573
|
-
|
|
574
|
+
// Any other failure (credentials, throttling, a region that can't be
|
|
575
|
+
// reached) establishes nothing about what is deployed. Reporting every
|
|
576
|
+
// declared entity as NOT-OBSERVED (#1089) is what keeps a broken read
|
|
577
|
+
// from arriving downstream as "none of this exists".
|
|
578
|
+
const reason = /credential|token|expired|AccessDenied|not authorized|UnauthorizedOperation/i.test(listResult.stderr)
|
|
579
|
+
? "no-credentials"
|
|
580
|
+
: "read-failed";
|
|
581
|
+
return observation(
|
|
582
|
+
{},
|
|
583
|
+
unobservedAll(
|
|
584
|
+
options.entityNames,
|
|
585
|
+
reason,
|
|
586
|
+
`describe-stack-resources failed for stack "${stackName}": ${listResult.stderr.trim().split("\n")[0] ?? ""}`,
|
|
587
|
+
),
|
|
588
|
+
);
|
|
574
589
|
}
|
|
575
590
|
|
|
576
591
|
const data = JSON.parse(listResult.stdout) as {
|
|
@@ -627,11 +642,19 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
|
|
|
627
642
|
physicalId: stackResource.PhysicalResourceId,
|
|
628
643
|
status: stackResource.ResourceStatus,
|
|
629
644
|
lastUpdated: stackResource.Timestamp,
|
|
645
|
+
// Total verdict (#1089): describe-stack-resources returns no tags, so
|
|
646
|
+
// this path cannot read the ownership marker. Say `unknown` explicitly
|
|
647
|
+
// rather than leaving the field off and letting each consumer guess —
|
|
648
|
+
// the change set never escalates `unknown` to a delete.
|
|
649
|
+
ownership: "unknown",
|
|
630
650
|
attributes: Object.keys(attributes).length > 0 ? attributes : undefined,
|
|
631
651
|
};
|
|
632
652
|
}
|
|
633
653
|
|
|
634
|
-
|
|
654
|
+
// Every entity the stack answered for was answered for: an entity the
|
|
655
|
+
// template doesn't carry is genuinely not in this stack, which is an
|
|
656
|
+
// absence, not a hole.
|
|
657
|
+
return observation(resources);
|
|
635
658
|
},
|
|
636
659
|
|
|
637
660
|
async describeStackStatus(options: { environment: string; stack: string }): Promise<StackStatusObservation | null> {
|