@intentius/chant-lexicon-aws 0.56.0 → 0.58.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/agentcore/trace-fetch.d.ts +5 -4
- package/dist/agentcore/trace-fetch.d.ts.map +1 -1
- package/dist/condition.d.ts +27 -0
- package/dist/condition.d.ts.map +1 -0
- package/dist/generated/index.d.ts +1 -1
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/import/generator.d.ts +19 -0
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts +19 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +4 -3
- package/dist/intrinsics.d.ts +77 -3
- package/dist/intrinsics.d.ts.map +1 -1
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +10 -0
- package/dist/lint/audit-lineage.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/waw069.d.ts +15 -0
- package/dist/lint/post-synth/waw069.d.ts.map +1 -0
- package/dist/manifest.json +29 -1
- package/dist/okf/index.md +1 -0
- package/dist/okf/rules/WAW069.md +11 -0
- package/dist/op/activities/aws-apply.d.ts.map +1 -1
- package/dist/op/activities/floci.d.ts +3 -2
- package/dist/op/activities/floci.d.ts.map +1 -1
- package/dist/op/builders.d.ts +4 -4
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/waw069.ts +0 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/package.json +2 -2
- package/src/agentcore/trace-fetch.ts +5 -4
- package/src/codegen/docs.ts +3 -3
- package/src/codegen/generate-typescript.ts +3 -3
- package/src/codegen/generate.ts +1 -1
- package/src/condition.test.ts +129 -0
- package/src/condition.ts +40 -0
- package/src/deep-observe.test.ts +23 -15
- package/src/deep-topology.test.ts +7 -3
- package/src/generated/index.d.ts +3 -3
- package/src/generated/index.ts +1 -1
- package/src/import/generator.ts +131 -50
- package/src/import/parser.test.ts +96 -0
- package/src/import/parser.ts +86 -1
- package/src/import/roundtrip-fixtures.test.ts +50 -0
- package/src/index.ts +15 -3
- package/src/intrinsics.ts +130 -5
- package/src/lint/audit-catalog.ts +6 -1
- package/src/lint/audit-lineage.ts +159 -0
- package/src/lint/post-synth/index.ts +2 -0
- package/src/lint/post-synth/waw069.test.ts +94 -0
- package/src/lint/post-synth/waw069.ts +0 -0
- package/src/op/activities/aws-apply.ts +1 -3
- package/src/op/activities/floci.ts +3 -2
- package/src/op/builders.ts +4 -4
- package/src/plugin.test.ts +5 -1
- package/src/plugin.ts +4 -0
- package/src/serializer.ts +24 -3
- package/src/testdata/roundtrip/with-conditions.json +16 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw069, checkUndeclaredConditions } from "./waw069";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW069: Template references an undeclared condition", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw069.id).toBe("WAW069");
|
|
12
|
+
expect(waw069.description).toContain("condition");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("resource Condition key referencing an undeclared condition → error", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
Rule: { Type: "AWS::Logs::LogGroup", Condition: "DoCutover", Properties: { RetentionInDays: 7 } },
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
const diags = checkUndeclaredConditions(ctx);
|
|
22
|
+
expect(diags).toHaveLength(1);
|
|
23
|
+
expect(diags[0].checkId).toBe("WAW069");
|
|
24
|
+
expect(diags[0].severity).toBe("error");
|
|
25
|
+
expect(diags[0].message).toContain("DoCutover");
|
|
26
|
+
expect(diags[0].entity).toBe("Rule");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("Fn::If nested in properties referencing an undeclared condition → error", () => {
|
|
30
|
+
const ctx = makeCtx({
|
|
31
|
+
Resources: {
|
|
32
|
+
Rule: {
|
|
33
|
+
Type: "AWS::Logs::LogGroup",
|
|
34
|
+
Properties: { LogGroupName: { "Fn::If": ["DoCutover", "/on", "/off"] } },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const diags = checkUndeclaredConditions(ctx);
|
|
39
|
+
expect(diags).toHaveLength(1);
|
|
40
|
+
expect(diags[0].message).toContain("Fn::If");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("output Condition key and Condition reference inside Conditions → error", () => {
|
|
44
|
+
const ctx = makeCtx({
|
|
45
|
+
Conditions: { NoCutover: { "Fn::Not": [{ Condition: "DoCutover" }] } },
|
|
46
|
+
Resources: { Rule: { Type: "AWS::Logs::LogGroup", Properties: {} } },
|
|
47
|
+
Outputs: { RuleName: { Condition: "AlsoMissing", Value: { Ref: "Rule" } } },
|
|
48
|
+
});
|
|
49
|
+
const diags = checkUndeclaredConditions(ctx);
|
|
50
|
+
expect(diags).toHaveLength(2);
|
|
51
|
+
const names = diags.map((d) => d.message);
|
|
52
|
+
expect(names.some((m) => m.includes('"DoCutover"'))).toBe(true);
|
|
53
|
+
expect(names.some((m) => m.includes('"AlsoMissing"'))).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("declared conditions are quiet", () => {
|
|
57
|
+
const ctx = makeCtx({
|
|
58
|
+
Conditions: {
|
|
59
|
+
DoCutover: { "Fn::Equals": [{ Ref: "Cutover" }, "true"] },
|
|
60
|
+
NoCutover: { "Fn::Not": [{ Condition: "DoCutover" }] },
|
|
61
|
+
},
|
|
62
|
+
Resources: {
|
|
63
|
+
Rule: {
|
|
64
|
+
Type: "AWS::Logs::LogGroup",
|
|
65
|
+
Condition: "DoCutover",
|
|
66
|
+
Properties: { LogGroupName: { "Fn::If": ["DoCutover", "/on", "/off"] } },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
Outputs: { RuleName: { Condition: "DoCutover", Value: { Ref: "Rule" } } },
|
|
70
|
+
});
|
|
71
|
+
expect(checkUndeclaredConditions(ctx)).toHaveLength(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("an IAM policy Condition block is not a condition reference", () => {
|
|
75
|
+
const ctx = makeCtx({
|
|
76
|
+
Resources: {
|
|
77
|
+
Role: {
|
|
78
|
+
Type: "AWS::IAM::Role",
|
|
79
|
+
Properties: {
|
|
80
|
+
AssumeRolePolicyDocument: {
|
|
81
|
+
Statement: [
|
|
82
|
+
{
|
|
83
|
+
Effect: "Allow",
|
|
84
|
+
Condition: { StringEquals: { "aws:SourceAccount": "123456789012" } },
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
expect(checkUndeclaredConditions(ctx)).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
Binary file
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { sleep } from "@intentius/chant/op";
|
|
3
3
|
import { awsDeployCapabilitiesForBody } from "../../components/cloud-executor.js";
|
|
4
4
|
import { resolveEndpointOverride } from "../../api/read-client.js";
|
|
5
5
|
import { ownershipStackTagsForBody } from "../../ownership.js";
|
|
@@ -128,7 +128,6 @@ export async function waitForStackSettled(
|
|
|
128
128
|
const deadline = Date.now() + opts.timeoutMs;
|
|
129
129
|
while (Date.now() < deadline) {
|
|
130
130
|
if (signal?.aborted) throw new Error("awsApply aborted");
|
|
131
|
-
safeHeartbeat({ step: "awsApply", stack: stackName });
|
|
132
131
|
const res = await http(url, cfnForm("DescribeStacks", { StackName: stackName }), signal);
|
|
133
132
|
const status = stackStatus(res.text);
|
|
134
133
|
if (status && isTerminalStatus(status)) return status;
|
|
@@ -231,7 +230,6 @@ export async function awsDelete(
|
|
|
231
230
|
const deadline = Date.now() + timeoutMs;
|
|
232
231
|
while (Date.now() < deadline) {
|
|
233
232
|
if (signal?.aborted) throw new Error("awsDelete aborted");
|
|
234
|
-
safeHeartbeat({ step: "awsDelete", stack: args.stackName });
|
|
235
233
|
const d = await http(url, cfnForm("DescribeStacks", { StackName: args.stackName }), signal);
|
|
236
234
|
if (d.status >= 300 && isStackMissing(d.text)) return { stackName: args.stackName, deleted: true };
|
|
237
235
|
const status = stackStatus(d.text);
|
|
@@ -101,8 +101,9 @@ export function flociRunCommand(args: FlociUpArgs = {}): string {
|
|
|
101
101
|
* Idempotent: reuses a running container of the same name. Waits for the health
|
|
102
102
|
* endpoint to report `readyService`, then sets `AWS_ENDPOINT_URL` + test creds in
|
|
103
103
|
* the process environment so a following `nativeApply`/`cfn-deploy` targets the
|
|
104
|
-
* emulator. Env injection
|
|
105
|
-
*
|
|
104
|
+
* emulator. Env injection works because the local executor runs every step of a
|
|
105
|
+
* run in this process; a step that runs anywhere else needs the endpoint passed
|
|
106
|
+
* to it explicitly.
|
|
106
107
|
*/
|
|
107
108
|
export async function flociUp(args: FlociUpArgs = {}, signal?: AbortSignal): Promise<{ endpoint: string }> {
|
|
108
109
|
const region = args.region ?? DEFAULT_REGION;
|
package/src/op/builders.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Typed step-builder wrappers for this lexicon's activities (chant #1288
|
|
3
3
|
* Stage 2). See `lexicons/k8s/src/op/builders.ts`'s module doc for why these
|
|
4
|
-
* live beside their `*Args` interfaces rather than in core
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* live beside their `*Args` interfaces rather than in core: core cannot
|
|
5
|
+
* import a lexicon's types (lexicons depend on core, not the reverse), so a
|
|
6
|
+
* wrapper whose `opts` IS the activity's own `*Args` interface can only sit
|
|
7
|
+
* in the lexicon that declares that interface. `opts`'s type in
|
|
8
8
|
* each wrapper below IS the activity's own `*Args` interface (via
|
|
9
9
|
* `Omit`/`WithStepRefs`) — never restated. `core`'s own
|
|
10
10
|
* `awsApply`/`awsDelete`/`flociUp`/`flociDown` are unchanged and produce
|
package/src/plugin.test.ts
CHANGED
|
@@ -48,11 +48,15 @@ describe("awsPlugin", () => {
|
|
|
48
48
|
|
|
49
49
|
test("returns intrinsics", () => {
|
|
50
50
|
const intrinsics = awsPlugin.intrinsics!();
|
|
51
|
-
expect(intrinsics.length).toBe(
|
|
51
|
+
expect(intrinsics.length).toBe(13);
|
|
52
52
|
const names = intrinsics.map((i) => i.name);
|
|
53
53
|
expect(names).toContain("Sub");
|
|
54
54
|
expect(names).toContain("Ref");
|
|
55
55
|
expect(names).toContain("GetAtt");
|
|
56
|
+
expect(names).toContain("Equals");
|
|
57
|
+
expect(names).toContain("And");
|
|
58
|
+
expect(names).toContain("Or");
|
|
59
|
+
expect(names).toContain("Not");
|
|
56
60
|
});
|
|
57
61
|
|
|
58
62
|
test("returns pseudo-parameters", () => {
|
package/src/plugin.ts
CHANGED
|
@@ -111,6 +111,10 @@ export const awsPlugin: LexiconPlugin = {
|
|
|
111
111
|
{ name: "Split", description: "Fn::Split — split string by delimiter", isTag: false, foldsAsCall: true },
|
|
112
112
|
{ name: "Base64", description: "Fn::Base64 — encode to Base64", isTag: false, foldsAsCall: true },
|
|
113
113
|
{ name: "GetAZs", description: "Fn::GetAZs — list Availability Zones", isTag: false, foldsAsCall: true },
|
|
114
|
+
{ name: "Equals", description: "Fn::Equals — condition comparing two values", isTag: false, foldsAsCall: true },
|
|
115
|
+
{ name: "And", description: "Fn::And — condition conjunction", isTag: false, foldsAsCall: true },
|
|
116
|
+
{ name: "Or", description: "Fn::Or — condition disjunction", isTag: false, foldsAsCall: true },
|
|
117
|
+
{ name: "Not", description: "Fn::Not — condition negation", isTag: false, foldsAsCall: true },
|
|
114
118
|
];
|
|
115
119
|
},
|
|
116
120
|
|
package/src/serializer.ts
CHANGED
|
@@ -19,8 +19,9 @@ import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
|
19
19
|
import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
|
|
20
20
|
import { isChildProject, type ChildProjectInstance } from "@intentius/chant/child-project";
|
|
21
21
|
import { isStackOutput, type StackOutput } from "@intentius/chant/stack-output";
|
|
22
|
-
import { isAttrRefLike } from "@intentius/chant/utils";
|
|
22
|
+
import { isAttrRefLike, getLogicalName } from "@intentius/chant/utils";
|
|
23
23
|
import { resolveDependsOn } from "@intentius/chant/resource-attributes";
|
|
24
|
+
import { isCondition } from "./condition";
|
|
24
25
|
import { isDefaultTags, type TagEntry } from "./default-tags";
|
|
25
26
|
import { isTemplateTransform } from "./template-transform";
|
|
26
27
|
import { loadTaggableResources } from "./taggable";
|
|
@@ -41,6 +42,7 @@ interface CFTemplate {
|
|
|
41
42
|
Metadata?: Record<string, unknown>;
|
|
42
43
|
Transform?: string | string[];
|
|
43
44
|
Parameters?: Record<string, CFParameter>;
|
|
45
|
+
Conditions?: Record<string, unknown>;
|
|
44
46
|
Resources: Record<string, CFResource>;
|
|
45
47
|
Outputs?: Record<string, CFOutput>;
|
|
46
48
|
}
|
|
@@ -347,6 +349,13 @@ function serializeToTemplate(
|
|
|
347
349
|
}
|
|
348
350
|
|
|
349
351
|
template.Parameters[name] = param;
|
|
352
|
+
} else if (isCondition(entity)) {
|
|
353
|
+
// Condition declarable → Conditions section (#2068), lifted the way
|
|
354
|
+
// Parameter is lifted into Parameters above.
|
|
355
|
+
if (!template.Conditions) {
|
|
356
|
+
template.Conditions = {};
|
|
357
|
+
}
|
|
358
|
+
template.Conditions[name] = toCFValue(entity.expression, entityNames);
|
|
350
359
|
} else if (isChildProject(entity)) {
|
|
351
360
|
// ChildProjectInstance → AWS::CloudFormation::Stack resource
|
|
352
361
|
const childProject = entity as ChildProjectInstance;
|
|
@@ -396,8 +405,13 @@ function serializeToTemplate(
|
|
|
396
405
|
resource.DependsOn = resolved.length === 1 ? resolved[0] : resolved;
|
|
397
406
|
}
|
|
398
407
|
}
|
|
399
|
-
// Pass-through attributes
|
|
400
|
-
|
|
408
|
+
// Pass-through attributes. Condition accepts the Condition declarable
|
|
409
|
+
// as well as a name string (#2068).
|
|
410
|
+
if (attrs.Condition) {
|
|
411
|
+
resource.Condition = isCondition(attrs.Condition)
|
|
412
|
+
? entityNames.get(attrs.Condition) ?? getLogicalName(attrs.Condition)
|
|
413
|
+
: attrs.Condition as string;
|
|
414
|
+
}
|
|
401
415
|
if (attrs.DeletionPolicy) resource.DeletionPolicy = attrs.DeletionPolicy as string;
|
|
402
416
|
if (attrs.UpdateReplacePolicy) resource.UpdateReplacePolicy = attrs.UpdateReplacePolicy as string;
|
|
403
417
|
if (attrs.UpdatePolicy) resource.UpdatePolicy = attrs.UpdatePolicy;
|
|
@@ -471,6 +485,13 @@ function serializeToTemplate(
|
|
|
471
485
|
if (exportName) {
|
|
472
486
|
output.Export = { Name: exportName };
|
|
473
487
|
}
|
|
488
|
+
// Same defensive read: condition (#2068/#2069) postdates older cores.
|
|
489
|
+
const condition = (stackOutput as { condition?: unknown }).condition;
|
|
490
|
+
if (condition) {
|
|
491
|
+
output.Condition = isCondition(condition)
|
|
492
|
+
? entityNames.get(condition) ?? getLogicalName(condition)
|
|
493
|
+
: condition as string;
|
|
494
|
+
}
|
|
474
495
|
template.Outputs[name] = output;
|
|
475
496
|
}
|
|
476
497
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"AWSTemplateFormatVersion": "2010-09-09",
|
|
3
|
+
"Parameters": { "Cutover": { "Type": "String", "Default": "false", "AllowedValues": ["true", "false"] } },
|
|
4
|
+
"Conditions": {
|
|
5
|
+
"DoCutover": { "Fn::Equals": [ { "Ref": "Cutover" }, "true" ] },
|
|
6
|
+
"NoCutover": { "Fn::Not": [ { "Condition": "DoCutover" } ] }
|
|
7
|
+
},
|
|
8
|
+
"Resources": {
|
|
9
|
+
"Rule": {
|
|
10
|
+
"Type": "AWS::Logs::LogGroup",
|
|
11
|
+
"Condition": "DoCutover",
|
|
12
|
+
"Properties": { "LogGroupName": { "Fn::If": [ "DoCutover", "/cut/on", "/cut/off" ] }, "RetentionInDays": 7 }
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"Outputs": { "RuleName": { "Condition": "DoCutover", "Value": { "Ref": "Rule" } } }
|
|
16
|
+
}
|