@intentius/chant-lexicon-aws 0.0.8 → 0.0.10
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 +25 -10
- package/dist/manifest.json +1 -1
- package/dist/meta.json +5743 -896
- package/dist/rules/cf-refs.ts +99 -0
- package/dist/rules/ext001.ts +30 -21
- package/dist/rules/hardcoded-region.ts +1 -0
- package/dist/rules/iam-wildcard.ts +1 -0
- package/dist/rules/s3-encryption.ts +1 -0
- package/dist/rules/waw016.ts +86 -0
- package/dist/rules/waw017.ts +53 -0
- package/dist/rules/waw018.ts +71 -0
- package/dist/rules/waw019.ts +82 -0
- package/dist/rules/waw020.ts +64 -0
- package/dist/rules/waw021.ts +53 -0
- package/dist/rules/waw022.ts +43 -0
- package/dist/rules/waw023.ts +47 -0
- package/dist/rules/waw024.ts +54 -0
- package/dist/rules/waw025.ts +43 -0
- package/dist/rules/waw026.ts +46 -0
- package/dist/rules/waw027.ts +50 -0
- package/dist/rules/waw028.ts +47 -0
- package/dist/rules/waw029.ts +62 -0
- package/dist/rules/waw030.ts +246 -0
- package/dist/skills/chant-aws.md +388 -30
- package/dist/types/index.d.ts +1552 -1528
- package/package.json +2 -2
- package/src/actions/actions.test.ts +75 -0
- package/src/actions/dynamodb.ts +36 -0
- package/src/actions/ecr.ts +9 -0
- package/src/actions/ecs.ts +5 -0
- package/src/actions/iam.ts +3 -0
- package/src/actions/index.ts +9 -0
- package/src/actions/lambda.ts +11 -0
- package/src/actions/logs.ts +4 -0
- package/src/actions/s3.ts +34 -0
- package/src/actions/sns.ts +5 -0
- package/src/actions/sqs.ts +15 -0
- package/src/codegen/__snapshots__/snapshot.test.ts.snap +2 -2
- package/src/codegen/docs-links.test.ts +143 -0
- package/src/codegen/docs.ts +247 -132
- package/src/codegen/generate-lexicon.ts +8 -0
- package/src/codegen/generate-typescript.ts +25 -1
- package/src/composites/composites.test.ts +442 -0
- package/src/composites/fargate-alb.ts +253 -0
- package/src/composites/index.ts +20 -0
- package/src/composites/lambda-api.ts +20 -0
- package/src/composites/lambda-dynamodb.ts +64 -0
- package/src/composites/lambda-eventbridge.ts +36 -0
- package/src/composites/lambda-function.ts +76 -0
- package/src/composites/lambda-s3.ts +72 -0
- package/src/composites/lambda-sns.ts +30 -0
- package/src/composites/lambda-sqs.ts +44 -0
- package/src/composites/scheduled-lambda.ts +37 -0
- package/src/composites/vpc-default.ts +148 -0
- package/src/default-tags.test.ts +38 -0
- package/src/default-tags.ts +77 -0
- package/src/generated/index.d.ts +1552 -1528
- package/src/generated/lexicon-aws.json +5743 -896
- package/src/import/roundtrip-fixtures.test.ts +1 -1
- package/src/index.ts +21 -0
- package/src/integration.test.ts +71 -0
- package/src/intrinsics.ts +24 -13
- package/src/lint/post-synth/cf-refs.ts +99 -0
- package/src/lint/post-synth/ext001.test.ts +214 -31
- package/src/lint/post-synth/ext001.ts +30 -21
- package/src/lint/post-synth/waw013.test.ts +120 -0
- package/src/lint/post-synth/waw014.test.ts +121 -0
- package/src/lint/post-synth/waw015.test.ts +147 -0
- package/src/lint/post-synth/waw016.test.ts +141 -0
- package/src/lint/post-synth/waw016.ts +86 -0
- package/src/lint/post-synth/waw017.test.ts +130 -0
- package/src/lint/post-synth/waw017.ts +53 -0
- package/src/lint/post-synth/waw018.test.ts +109 -0
- package/src/lint/post-synth/waw018.ts +71 -0
- package/src/lint/post-synth/waw019.test.ts +138 -0
- package/src/lint/post-synth/waw019.ts +82 -0
- package/src/lint/post-synth/waw020.test.ts +125 -0
- package/src/lint/post-synth/waw020.ts +64 -0
- package/src/lint/post-synth/waw021.test.ts +81 -0
- package/src/lint/post-synth/waw021.ts +53 -0
- package/src/lint/post-synth/waw022.test.ts +54 -0
- package/src/lint/post-synth/waw022.ts +43 -0
- package/src/lint/post-synth/waw023.test.ts +53 -0
- package/src/lint/post-synth/waw023.ts +47 -0
- package/src/lint/post-synth/waw024.test.ts +64 -0
- package/src/lint/post-synth/waw024.ts +54 -0
- package/src/lint/post-synth/waw025.test.ts +42 -0
- package/src/lint/post-synth/waw025.ts +43 -0
- package/src/lint/post-synth/waw026.test.ts +54 -0
- package/src/lint/post-synth/waw026.ts +46 -0
- package/src/lint/post-synth/waw027.test.ts +63 -0
- package/src/lint/post-synth/waw027.ts +50 -0
- package/src/lint/post-synth/waw028.test.ts +68 -0
- package/src/lint/post-synth/waw028.ts +47 -0
- package/src/lint/post-synth/waw029.test.ts +179 -0
- package/src/lint/post-synth/waw029.ts +62 -0
- package/src/lint/post-synth/waw030.test.ts +800 -0
- package/src/lint/post-synth/waw030.ts +246 -0
- package/src/lint/rules/hardcoded-region.ts +1 -0
- package/src/lint/rules/iam-wildcard.ts +1 -0
- package/src/lint/rules/s3-encryption.ts +1 -0
- package/src/lsp/hover.ts +15 -0
- package/src/nested-stack-integration.test.ts +100 -0
- package/src/nested-stack.ts +1 -1
- package/src/plugin.ts +468 -36
- package/src/serializer.test.ts +330 -2
- package/src/serializer.ts +62 -1
- package/src/spec/fetch.ts +10 -0
- package/src/spec/parse.test.ts +141 -0
- package/src/spec/parse.ts +40 -0
- package/src/taggable.ts +44 -0
- package/src/testdata/nested-stacks/app.ts +26 -0
- package/src/testdata/nested-stacks/network/outputs.ts +17 -0
- package/src/testdata/nested-stacks/network/security.ts +17 -0
- package/src/testdata/nested-stacks/network/vpc.ts +54 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from "bun:test";
|
|
2
|
+
import { expandComposite, CompositeRegistry, isCompositeInstance } from "@intentius/chant";
|
|
3
|
+
import { AttrRef } from "@intentius/chant/attrref";
|
|
4
|
+
import { LambdaFunction, LambdaNode, LambdaPython, NodeLambda, PythonLambda } from "./lambda-function";
|
|
5
|
+
import { LambdaApi } from "./lambda-api";
|
|
6
|
+
import { LambdaScheduled, ScheduledLambda } from "./scheduled-lambda";
|
|
7
|
+
import { LambdaSqs } from "./lambda-sqs";
|
|
8
|
+
import { LambdaEventBridge } from "./lambda-eventbridge";
|
|
9
|
+
import { LambdaDynamoDB } from "./lambda-dynamodb";
|
|
10
|
+
import { LambdaS3 } from "./lambda-s3";
|
|
11
|
+
import { LambdaSns } from "./lambda-sns";
|
|
12
|
+
import { VpcDefault } from "./vpc-default";
|
|
13
|
+
import { FargateAlb } from "./fargate-alb";
|
|
14
|
+
|
|
15
|
+
const baseProps = {
|
|
16
|
+
name: "TestFunc",
|
|
17
|
+
Runtime: "nodejs20.x",
|
|
18
|
+
Handler: "index.handler",
|
|
19
|
+
Code: { ZipFile: "exports.handler = async () => ({statusCode:200})" },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe("LambdaFunction", () => {
|
|
23
|
+
test("returns role and func members", () => {
|
|
24
|
+
const instance = LambdaFunction(baseProps);
|
|
25
|
+
expect(instance.role).toBeDefined();
|
|
26
|
+
expect(instance.func).toBeDefined();
|
|
27
|
+
expect(Object.keys(instance.members)).toEqual(["role", "func"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("func.Role references role.Arn via AttrRef", () => {
|
|
31
|
+
const instance = LambdaFunction(baseProps);
|
|
32
|
+
const funcProps = (instance.func as any).props;
|
|
33
|
+
expect(funcProps.Role).toBeInstanceOf(AttrRef);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("expandComposite produces correct logical names", () => {
|
|
37
|
+
const instance = LambdaFunction(baseProps);
|
|
38
|
+
const expanded = expandComposite("myLambda", instance);
|
|
39
|
+
expect(expanded.has("myLambdaRole")).toBe(true);
|
|
40
|
+
expect(expanded.has("myLambdaFunc")).toBe(true);
|
|
41
|
+
expect(expanded.size).toBe(2);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("default timeout is 30", () => {
|
|
45
|
+
const instance = LambdaFunction(baseProps);
|
|
46
|
+
const funcProps = (instance.func as any).props;
|
|
47
|
+
expect(funcProps.Timeout).toBe(30);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("VpcConfig auto-attaches VPCAccessExecutionRole", () => {
|
|
51
|
+
const instance = LambdaFunction({
|
|
52
|
+
...baseProps,
|
|
53
|
+
VpcConfig: { SubnetIds: ["subnet-1"], SecurityGroupIds: ["sg-1"] },
|
|
54
|
+
});
|
|
55
|
+
const roleProps = (instance.role as any).props;
|
|
56
|
+
expect(roleProps.ManagedPolicyArns).toContain(
|
|
57
|
+
"arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole",
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("without VpcConfig, no VPCAccessExecutionRole", () => {
|
|
62
|
+
const instance = LambdaFunction(baseProps);
|
|
63
|
+
const roleProps = (instance.role as any).props;
|
|
64
|
+
expect(roleProps.ManagedPolicyArns).not.toContain(
|
|
65
|
+
"arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole",
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("additional ManagedPolicyArns are appended", () => {
|
|
70
|
+
const customArn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess";
|
|
71
|
+
const instance = LambdaFunction({
|
|
72
|
+
...baseProps,
|
|
73
|
+
ManagedPolicyArns: [customArn],
|
|
74
|
+
});
|
|
75
|
+
const roleProps = (instance.role as any).props;
|
|
76
|
+
expect(roleProps.ManagedPolicyArns).toContain(customArn);
|
|
77
|
+
expect(roleProps.ManagedPolicyArns).toContain(
|
|
78
|
+
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("LambdaNode / LambdaPython presets", () => {
|
|
84
|
+
test("LambdaNode defaults Runtime and Handler", () => {
|
|
85
|
+
const instance = LambdaNode({
|
|
86
|
+
name: "TestNode",
|
|
87
|
+
Code: { ZipFile: "exports.handler = async () => ({})" },
|
|
88
|
+
});
|
|
89
|
+
const funcProps = (instance.func as any).props;
|
|
90
|
+
expect(funcProps.Runtime).toBe("nodejs20.x");
|
|
91
|
+
expect(funcProps.Handler).toBe("index.handler");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("LambdaPython defaults Runtime and Handler", () => {
|
|
95
|
+
const instance = LambdaPython({
|
|
96
|
+
name: "TestPython",
|
|
97
|
+
Code: { ZipFile: "def handler(event, context): return {}" },
|
|
98
|
+
});
|
|
99
|
+
const funcProps = (instance.func as any).props;
|
|
100
|
+
expect(funcProps.Runtime).toBe("python3.12");
|
|
101
|
+
expect(funcProps.Handler).toBe("handler.handler");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("preset defaults can be overridden", () => {
|
|
105
|
+
const instance = LambdaNode({
|
|
106
|
+
name: "TestOverride",
|
|
107
|
+
Runtime: "nodejs18.x",
|
|
108
|
+
Code: { ZipFile: "" },
|
|
109
|
+
});
|
|
110
|
+
const funcProps = (instance.func as any).props;
|
|
111
|
+
expect(funcProps.Runtime).toBe("nodejs18.x");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("deprecated aliases still work", () => {
|
|
115
|
+
expect(NodeLambda).toBe(LambdaNode);
|
|
116
|
+
expect(PythonLambda).toBe(LambdaPython);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("LambdaApi", () => {
|
|
121
|
+
test("returns role, func, and permission members", () => {
|
|
122
|
+
const instance = LambdaApi(baseProps);
|
|
123
|
+
expect(Object.keys(instance.members)).toEqual(["role", "func", "permission"]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("permission references func.Arn", () => {
|
|
127
|
+
const instance = LambdaApi(baseProps);
|
|
128
|
+
const permProps = (instance.permission as any).props;
|
|
129
|
+
expect(permProps.FunctionName).toBeInstanceOf(AttrRef);
|
|
130
|
+
expect(permProps.Principal).toBe("apigateway.amazonaws.com");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("sourceArn is passed through", () => {
|
|
134
|
+
const instance = LambdaApi({
|
|
135
|
+
...baseProps,
|
|
136
|
+
sourceArn: "arn:aws:execute-api:us-east-1:123:api/*",
|
|
137
|
+
});
|
|
138
|
+
const permProps = (instance.permission as any).props;
|
|
139
|
+
expect(permProps.SourceArn).toBe("arn:aws:execute-api:us-east-1:123:api/*");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("expandComposite produces 3 entries", () => {
|
|
143
|
+
const expanded = expandComposite("api", LambdaApi(baseProps));
|
|
144
|
+
expect(expanded.size).toBe(3);
|
|
145
|
+
expect(expanded.has("apiRole")).toBe(true);
|
|
146
|
+
expect(expanded.has("apiFunc")).toBe(true);
|
|
147
|
+
expect(expanded.has("apiPermission")).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("LambdaScheduled", () => {
|
|
152
|
+
const scheduledProps = { ...baseProps, schedule: "rate(5 minutes)" };
|
|
153
|
+
|
|
154
|
+
test("returns role, func, rule, and permission members", () => {
|
|
155
|
+
const instance = LambdaScheduled(scheduledProps);
|
|
156
|
+
expect(Object.keys(instance.members)).toEqual(["role", "func", "rule", "permission"]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("rule has ScheduleExpression and targets func", () => {
|
|
160
|
+
const instance = LambdaScheduled(scheduledProps);
|
|
161
|
+
const ruleProps = (instance.rule as any).props;
|
|
162
|
+
expect(ruleProps.ScheduleExpression).toBe("rate(5 minutes)");
|
|
163
|
+
expect(ruleProps.State).toBe("ENABLED");
|
|
164
|
+
expect(ruleProps.Targets).toHaveLength(1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("enabled: false sets State to DISABLED", () => {
|
|
168
|
+
const instance = LambdaScheduled({ ...scheduledProps, enabled: false });
|
|
169
|
+
const ruleProps = (instance.rule as any).props;
|
|
170
|
+
expect(ruleProps.State).toBe("DISABLED");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("permission principal is events.amazonaws.com", () => {
|
|
174
|
+
const instance = LambdaScheduled(scheduledProps);
|
|
175
|
+
const permProps = (instance.permission as any).props;
|
|
176
|
+
expect(permProps.Principal).toBe("events.amazonaws.com");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("expandComposite produces 4 entries", () => {
|
|
180
|
+
const expanded = expandComposite("cron", LambdaScheduled(scheduledProps));
|
|
181
|
+
expect(expanded.size).toBe(4);
|
|
182
|
+
expect(expanded.has("cronRole")).toBe(true);
|
|
183
|
+
expect(expanded.has("cronFunc")).toBe(true);
|
|
184
|
+
expect(expanded.has("cronRule")).toBe(true);
|
|
185
|
+
expect(expanded.has("cronPermission")).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("deprecated ScheduledLambda alias still works", () => {
|
|
189
|
+
expect(ScheduledLambda).toBe(LambdaScheduled);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("LambdaSqs", () => {
|
|
194
|
+
test("returns queue, role, func members", () => {
|
|
195
|
+
const instance = LambdaSqs(baseProps);
|
|
196
|
+
expect(Object.keys(instance.members)).toEqual(["queue", "role", "func"]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("expandComposite produces 4 entries (queue + role + func + eventSourceMapping)", () => {
|
|
200
|
+
const expanded = expandComposite("worker", LambdaSqs(baseProps));
|
|
201
|
+
expect(expanded.has("workerQueue")).toBe(true);
|
|
202
|
+
expect(expanded.has("workerRole")).toBe(true);
|
|
203
|
+
expect(expanded.has("workerFunc")).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("LambdaEventBridge", () => {
|
|
208
|
+
test("returns rule, role, func, permission members", () => {
|
|
209
|
+
const instance = LambdaEventBridge({ ...baseProps, schedule: "rate(1 hour)" });
|
|
210
|
+
expect(Object.keys(instance.members)).toEqual(["rule", "role", "func", "permission"]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("supports eventPattern", () => {
|
|
214
|
+
const instance = LambdaEventBridge({
|
|
215
|
+
...baseProps,
|
|
216
|
+
eventPattern: { source: ["aws.s3"] },
|
|
217
|
+
});
|
|
218
|
+
const ruleProps = (instance.rule as any).props;
|
|
219
|
+
expect(ruleProps.EventPattern).toEqual({ source: ["aws.s3"] });
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("LambdaDynamoDB", () => {
|
|
224
|
+
test("returns table, role, func members", () => {
|
|
225
|
+
const instance = LambdaDynamoDB({ ...baseProps, partitionKey: "pk" });
|
|
226
|
+
expect(Object.keys(instance.members)).toEqual(["table", "role", "func"]);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("creates sort key when specified", () => {
|
|
230
|
+
const instance = LambdaDynamoDB({ ...baseProps, partitionKey: "pk", sortKey: "sk" });
|
|
231
|
+
const tableProps = (instance.table as any).props;
|
|
232
|
+
expect(tableProps.AttributeDefinitions).toHaveLength(2);
|
|
233
|
+
expect(tableProps.KeySchema).toHaveLength(2);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("LambdaS3", () => {
|
|
238
|
+
test("returns bucket, role, func members", () => {
|
|
239
|
+
const instance = LambdaS3(baseProps);
|
|
240
|
+
expect(Object.keys(instance.members)).toEqual(["bucket", "role", "func"]);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("bucket has encryption and public access block", () => {
|
|
244
|
+
const instance = LambdaS3(baseProps);
|
|
245
|
+
const bucketProps = (instance.bucket as any).props;
|
|
246
|
+
expect(bucketProps.BucketEncryption).toBeDefined();
|
|
247
|
+
expect(bucketProps.PublicAccessBlockConfiguration).toBeDefined();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe("LambdaSns", () => {
|
|
252
|
+
test("returns topic, role, func, subscription, permission members", () => {
|
|
253
|
+
const instance = LambdaSns(baseProps);
|
|
254
|
+
expect(Object.keys(instance.members)).toEqual(["topic", "role", "func", "subscription", "permission"]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("subscription uses lambda protocol", () => {
|
|
258
|
+
const instance = LambdaSns(baseProps);
|
|
259
|
+
const subProps = (instance.subscription as any).props;
|
|
260
|
+
expect(subProps.Protocol).toBe("lambda");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("permission principal is sns.amazonaws.com", () => {
|
|
264
|
+
const instance = LambdaSns(baseProps);
|
|
265
|
+
const permProps = (instance.permission as any).props;
|
|
266
|
+
expect(permProps.Principal).toBe("sns.amazonaws.com");
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe("VpcDefault", () => {
|
|
271
|
+
test("returns 17 members", () => {
|
|
272
|
+
const instance = VpcDefault({});
|
|
273
|
+
expect(Object.keys(instance.members)).toHaveLength(17);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("has all expected member names", () => {
|
|
277
|
+
const instance = VpcDefault({});
|
|
278
|
+
const names = Object.keys(instance.members);
|
|
279
|
+
expect(names).toContain("vpc");
|
|
280
|
+
expect(names).toContain("igw");
|
|
281
|
+
expect(names).toContain("igwAttachment");
|
|
282
|
+
expect(names).toContain("publicSubnet1");
|
|
283
|
+
expect(names).toContain("publicSubnet2");
|
|
284
|
+
expect(names).toContain("privateSubnet1");
|
|
285
|
+
expect(names).toContain("privateSubnet2");
|
|
286
|
+
expect(names).toContain("publicRouteTable");
|
|
287
|
+
expect(names).toContain("publicRoute");
|
|
288
|
+
expect(names).toContain("publicRta1");
|
|
289
|
+
expect(names).toContain("publicRta2");
|
|
290
|
+
expect(names).toContain("privateRouteTable");
|
|
291
|
+
expect(names).toContain("privateRta1");
|
|
292
|
+
expect(names).toContain("privateRta2");
|
|
293
|
+
expect(names).toContain("natEip");
|
|
294
|
+
expect(names).toContain("natGateway");
|
|
295
|
+
expect(names).toContain("privateRoute");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("VPC has DNS enabled", () => {
|
|
299
|
+
const instance = VpcDefault({});
|
|
300
|
+
const vpcProps = (instance.vpc as any).props;
|
|
301
|
+
expect(vpcProps.EnableDnsSupport).toBe(true);
|
|
302
|
+
expect(vpcProps.EnableDnsHostnames).toBe(true);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("public subnets have MapPublicIpOnLaunch", () => {
|
|
306
|
+
const instance = VpcDefault({});
|
|
307
|
+
const pub1Props = (instance.publicSubnet1 as any).props;
|
|
308
|
+
const pub2Props = (instance.publicSubnet2 as any).props;
|
|
309
|
+
expect(pub1Props.MapPublicIpOnLaunch).toBe(true);
|
|
310
|
+
expect(pub2Props.MapPublicIpOnLaunch).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("vpc.VpcId is wired to subnets", () => {
|
|
314
|
+
const instance = VpcDefault({});
|
|
315
|
+
const sub1Props = (instance.publicSubnet1 as any).props;
|
|
316
|
+
expect(sub1Props.VpcId).toBeInstanceOf(AttrRef);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("NAT gateway is present", () => {
|
|
320
|
+
const instance = VpcDefault({});
|
|
321
|
+
expect(instance.natGateway).toBeDefined();
|
|
322
|
+
expect(instance.natEip).toBeDefined();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("expandComposite produces 17 entries", () => {
|
|
326
|
+
const expanded = expandComposite("net", VpcDefault({}));
|
|
327
|
+
expect(expanded.size).toBe(17);
|
|
328
|
+
expect(expanded.has("netVpc")).toBe(true);
|
|
329
|
+
expect(expanded.has("netIgw")).toBe(true);
|
|
330
|
+
expect(expanded.has("netNatGateway")).toBe(true);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("custom CIDR overrides defaults", () => {
|
|
334
|
+
const instance = VpcDefault({ cidr: "172.16.0.0/16" });
|
|
335
|
+
const vpcProps = (instance.vpc as any).props;
|
|
336
|
+
expect(vpcProps.CidrBlock).toBe("172.16.0.0/16");
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe("FargateAlb", () => {
|
|
341
|
+
const fargateProps = {
|
|
342
|
+
image: "nginx:latest",
|
|
343
|
+
vpcId: "vpc-123",
|
|
344
|
+
publicSubnetIds: ["subnet-pub1", "subnet-pub2"],
|
|
345
|
+
privateSubnetIds: ["subnet-priv1", "subnet-priv2"],
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
test("returns 11 members", () => {
|
|
349
|
+
const instance = FargateAlb(fargateProps);
|
|
350
|
+
expect(Object.keys(instance.members)).toHaveLength(11);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("has all expected member names", () => {
|
|
354
|
+
const instance = FargateAlb(fargateProps);
|
|
355
|
+
const names = Object.keys(instance.members);
|
|
356
|
+
expect(names).toEqual([
|
|
357
|
+
"cluster", "executionRole", "taskRole", "logGroup", "taskDef",
|
|
358
|
+
"albSg", "taskSg", "alb", "targetGroup", "listener", "service",
|
|
359
|
+
]);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("expandComposite produces correct logical names", () => {
|
|
363
|
+
const expanded = expandComposite("web", FargateAlb(fargateProps));
|
|
364
|
+
expect(expanded.has("webCluster")).toBe(true);
|
|
365
|
+
expect(expanded.has("webExecutionRole")).toBe(true);
|
|
366
|
+
expect(expanded.has("webTaskRole")).toBe(true);
|
|
367
|
+
expect(expanded.has("webLogGroup")).toBe(true);
|
|
368
|
+
expect(expanded.has("webTaskDef")).toBe(true);
|
|
369
|
+
expect(expanded.has("webAlbSg")).toBe(true);
|
|
370
|
+
expect(expanded.has("webTaskSg")).toBe(true);
|
|
371
|
+
expect(expanded.has("webAlb")).toBe(true);
|
|
372
|
+
expect(expanded.has("webTargetGroup")).toBe(true);
|
|
373
|
+
expect(expanded.has("webListener")).toBe(true);
|
|
374
|
+
expect(expanded.has("webService")).toBe(true);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("execution role has ECR and Logs policies", () => {
|
|
378
|
+
const instance = FargateAlb(fargateProps);
|
|
379
|
+
const roleProps = (instance.executionRole as any).props;
|
|
380
|
+
expect(roleProps.Policies).toHaveLength(1);
|
|
381
|
+
const policyDoc = (roleProps.Policies[0] as any).props.PolicyDocument;
|
|
382
|
+
expect(policyDoc.Statement).toHaveLength(2);
|
|
383
|
+
expect(policyDoc.Statement[0].Action).toContain("ecr:GetAuthorizationToken");
|
|
384
|
+
expect(policyDoc.Statement[1].Action).toContain("logs:CreateLogStream");
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("task role receives custom policies", () => {
|
|
388
|
+
const { Role_Policy } = require("../generated");
|
|
389
|
+
const customPolicy = new Role_Policy({
|
|
390
|
+
PolicyName: "Custom",
|
|
391
|
+
PolicyDocument: { Version: "2012-10-17", Statement: [] },
|
|
392
|
+
});
|
|
393
|
+
const instance = FargateAlb({ ...fargateProps, Policies: [customPolicy] });
|
|
394
|
+
const roleProps = (instance.taskRole as any).props;
|
|
395
|
+
expect(roleProps.Policies).toHaveLength(1);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("task definition has awsvpc and FARGATE", () => {
|
|
399
|
+
const instance = FargateAlb(fargateProps);
|
|
400
|
+
const tdProps = (instance.taskDef as any).props;
|
|
401
|
+
expect(tdProps.NetworkMode).toBe("awsvpc");
|
|
402
|
+
expect(tdProps.RequiresCompatibilities).toEqual(["FARGATE"]);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("ALB SG allows ingress on listener port", () => {
|
|
406
|
+
const instance = FargateAlb(fargateProps);
|
|
407
|
+
const sgProps = (instance.albSg as any).props;
|
|
408
|
+
expect(sgProps.SecurityGroupIngress).toHaveLength(1);
|
|
409
|
+
const ingress = (sgProps.SecurityGroupIngress[0] as any).props;
|
|
410
|
+
expect(ingress.FromPort).toBe(80);
|
|
411
|
+
expect(ingress.CidrIp).toBe("0.0.0.0/0");
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("task SG references ALB SG GroupId", () => {
|
|
415
|
+
const instance = FargateAlb(fargateProps);
|
|
416
|
+
const sgProps = (instance.taskSg as any).props;
|
|
417
|
+
const ingress = (sgProps.SecurityGroupIngress[0] as any).props;
|
|
418
|
+
expect(ingress.SourceSecurityGroupId).toBeInstanceOf(AttrRef);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("default container port is 80", () => {
|
|
422
|
+
const instance = FargateAlb(fargateProps);
|
|
423
|
+
const tdProps = (instance.taskDef as any).props;
|
|
424
|
+
const containerDef = (tdProps.ContainerDefinitions[0] as any).props;
|
|
425
|
+
const portMapping = (containerDef.PortMappings[0] as any).props;
|
|
426
|
+
expect(portMapping.ContainerPort).toBe(80);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("default desired count is 2", () => {
|
|
430
|
+
const instance = FargateAlb(fargateProps);
|
|
431
|
+
const svcProps = (instance.service as any).props;
|
|
432
|
+
expect(svcProps.DesiredCount).toBe(2);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("custom listener port is applied", () => {
|
|
436
|
+
const instance = FargateAlb({ ...fargateProps, listenerPort: 8080 });
|
|
437
|
+
const sgProps = (instance.albSg as any).props;
|
|
438
|
+
const ingress = (sgProps.SecurityGroupIngress[0] as any).props;
|
|
439
|
+
expect(ingress.FromPort).toBe(8080);
|
|
440
|
+
expect(ingress.ToPort).toBe(8080);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Composite } from "@intentius/chant";
|
|
2
|
+
import {
|
|
3
|
+
EcsCluster,
|
|
4
|
+
EcsService,
|
|
5
|
+
EcsService_LoadBalancer,
|
|
6
|
+
EcsService_NetworkConfiguration,
|
|
7
|
+
EcsService_AwsVpcConfiguration,
|
|
8
|
+
TaskDefinition,
|
|
9
|
+
TaskDefinition_ContainerDefinition,
|
|
10
|
+
TaskDefinition_PortMapping,
|
|
11
|
+
TaskDefinition_LogConfiguration,
|
|
12
|
+
TaskDefinition_KeyValuePair,
|
|
13
|
+
LoadBalancer,
|
|
14
|
+
TargetGroup,
|
|
15
|
+
Listener,
|
|
16
|
+
Listener_Action,
|
|
17
|
+
SecurityGroup,
|
|
18
|
+
SecurityGroup_Ingress,
|
|
19
|
+
LogGroup,
|
|
20
|
+
Role,
|
|
21
|
+
Role_Policy,
|
|
22
|
+
} from "../generated";
|
|
23
|
+
import { Sub } from "../intrinsics";
|
|
24
|
+
import { ECRActions } from "../actions/ecr";
|
|
25
|
+
import { LogsActions } from "../actions/logs";
|
|
26
|
+
|
|
27
|
+
export interface FargateAlbProps {
|
|
28
|
+
image: string;
|
|
29
|
+
containerPort?: number;
|
|
30
|
+
cpu?: string;
|
|
31
|
+
memory?: string;
|
|
32
|
+
desiredCount?: number;
|
|
33
|
+
vpcId: string;
|
|
34
|
+
publicSubnetIds: string[];
|
|
35
|
+
privateSubnetIds: string[];
|
|
36
|
+
healthCheckPath?: string;
|
|
37
|
+
listenerPort?: number;
|
|
38
|
+
environment?: Record<string, string>;
|
|
39
|
+
command?: string[];
|
|
40
|
+
ManagedPolicyArns?: string[];
|
|
41
|
+
Policies?: InstanceType<typeof Role_Policy>[];
|
|
42
|
+
logRetentionDays?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ecsTrustPolicy = {
|
|
46
|
+
Version: "2012-10-17" as const,
|
|
47
|
+
Statement: [
|
|
48
|
+
{
|
|
49
|
+
Effect: "Allow" as const,
|
|
50
|
+
Principal: { Service: "ecs-tasks.amazonaws.com" },
|
|
51
|
+
Action: "sts:AssumeRole",
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const FargateAlb = Composite<FargateAlbProps>((props) => {
|
|
57
|
+
const containerPort = props.containerPort ?? 80;
|
|
58
|
+
const cpu = props.cpu ?? "256";
|
|
59
|
+
const memory = props.memory ?? "512";
|
|
60
|
+
const desiredCount = props.desiredCount ?? 2;
|
|
61
|
+
const healthCheckPath = props.healthCheckPath ?? "/";
|
|
62
|
+
const listenerPort = props.listenerPort ?? 80;
|
|
63
|
+
const logRetentionDays = props.logRetentionDays ?? 30;
|
|
64
|
+
|
|
65
|
+
// ECS Cluster
|
|
66
|
+
const cluster = new EcsCluster({});
|
|
67
|
+
|
|
68
|
+
// Execution role — ECR pull + CloudWatch Logs write
|
|
69
|
+
const executionPolicyDocument = {
|
|
70
|
+
Version: "2012-10-17",
|
|
71
|
+
Statement: [
|
|
72
|
+
{
|
|
73
|
+
Effect: "Allow",
|
|
74
|
+
Action: ECRActions.Pull,
|
|
75
|
+
Resource: "*",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
Effect: "Allow",
|
|
79
|
+
Action: LogsActions.Write,
|
|
80
|
+
Resource: "*",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const executionPolicy = new Role_Policy({
|
|
86
|
+
PolicyName: "ExecutionPolicy",
|
|
87
|
+
PolicyDocument: executionPolicyDocument,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const executionRole = new Role({
|
|
91
|
+
AssumeRolePolicyDocument: ecsTrustPolicy,
|
|
92
|
+
Policies: [executionPolicy],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Task role — app permissions
|
|
96
|
+
const taskRole = new Role({
|
|
97
|
+
AssumeRolePolicyDocument: ecsTrustPolicy,
|
|
98
|
+
ManagedPolicyArns: props.ManagedPolicyArns,
|
|
99
|
+
Policies: props.Policies,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Log group
|
|
103
|
+
const logGroup = new LogGroup({
|
|
104
|
+
RetentionInDays: logRetentionDays,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Container definition
|
|
108
|
+
const portMapping = new TaskDefinition_PortMapping({
|
|
109
|
+
ContainerPort: containerPort,
|
|
110
|
+
Protocol: "tcp",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const logConfiguration = new TaskDefinition_LogConfiguration({
|
|
114
|
+
LogDriver: "awslogs",
|
|
115
|
+
Options: {
|
|
116
|
+
"awslogs-group": logGroup as any,
|
|
117
|
+
"awslogs-region": Sub`\${AWS::Region}`,
|
|
118
|
+
"awslogs-stream-prefix": "ecs",
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const environmentVars: InstanceType<typeof TaskDefinition_KeyValuePair>[] = [];
|
|
123
|
+
if (props.environment) {
|
|
124
|
+
for (const [name, value] of Object.entries(props.environment)) {
|
|
125
|
+
environmentVars.push(
|
|
126
|
+
new TaskDefinition_KeyValuePair({ Name: name, Value: value }),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const container = new TaskDefinition_ContainerDefinition({
|
|
132
|
+
Name: "app",
|
|
133
|
+
Image: props.image,
|
|
134
|
+
Essential: true,
|
|
135
|
+
PortMappings: [portMapping],
|
|
136
|
+
LogConfiguration: logConfiguration,
|
|
137
|
+
Environment: environmentVars.length > 0 ? environmentVars : undefined,
|
|
138
|
+
Command: props.command,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Task definition
|
|
142
|
+
const taskDef = new TaskDefinition({
|
|
143
|
+
NetworkMode: "awsvpc",
|
|
144
|
+
RequiresCompatibilities: ["FARGATE"],
|
|
145
|
+
Cpu: cpu,
|
|
146
|
+
Memory: memory,
|
|
147
|
+
ExecutionRoleArn: executionRole.Arn,
|
|
148
|
+
TaskRoleArn: taskRole.Arn,
|
|
149
|
+
ContainerDefinitions: [container],
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ALB security group — ingress on listener port from anywhere
|
|
153
|
+
const albIngress = new SecurityGroup_Ingress({
|
|
154
|
+
IpProtocol: "tcp",
|
|
155
|
+
FromPort: listenerPort,
|
|
156
|
+
ToPort: listenerPort,
|
|
157
|
+
CidrIp: "0.0.0.0/0",
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const albSg = new SecurityGroup({
|
|
161
|
+
GroupDescription: "ALB security group",
|
|
162
|
+
VpcId: props.vpcId,
|
|
163
|
+
SecurityGroupIngress: [albIngress],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Task security group — ingress on container port from ALB SG
|
|
167
|
+
const taskIngress = new SecurityGroup_Ingress({
|
|
168
|
+
IpProtocol: "tcp",
|
|
169
|
+
FromPort: containerPort,
|
|
170
|
+
ToPort: containerPort,
|
|
171
|
+
SourceSecurityGroupId: albSg.GroupId,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const taskSg = new SecurityGroup({
|
|
175
|
+
GroupDescription: "Fargate task security group",
|
|
176
|
+
VpcId: props.vpcId,
|
|
177
|
+
SecurityGroupIngress: [taskIngress],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Application Load Balancer
|
|
181
|
+
const alb = new LoadBalancer({
|
|
182
|
+
Type: "application",
|
|
183
|
+
Scheme: "internet-facing",
|
|
184
|
+
Subnets: props.publicSubnetIds,
|
|
185
|
+
SecurityGroups: [albSg.GroupId],
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Target group
|
|
189
|
+
const targetGroup = new TargetGroup({
|
|
190
|
+
TargetType: "ip",
|
|
191
|
+
Protocol: "HTTP",
|
|
192
|
+
Port: containerPort,
|
|
193
|
+
VpcId: props.vpcId,
|
|
194
|
+
HealthCheckPath: healthCheckPath,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Listener
|
|
198
|
+
const defaultAction = new Listener_Action({
|
|
199
|
+
Type: "forward",
|
|
200
|
+
TargetGroupArn: targetGroup.TargetGroupArn,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const listener = new Listener({
|
|
204
|
+
LoadBalancerArn: alb.LoadBalancerArn,
|
|
205
|
+
Port: listenerPort,
|
|
206
|
+
Protocol: "HTTP",
|
|
207
|
+
DefaultActions: [defaultAction],
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ECS Service
|
|
211
|
+
const serviceLoadBalancer = new EcsService_LoadBalancer({
|
|
212
|
+
ContainerName: "app",
|
|
213
|
+
ContainerPort: containerPort,
|
|
214
|
+
TargetGroupArn: targetGroup.TargetGroupArn,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const awsVpcConfig = new EcsService_AwsVpcConfiguration({
|
|
218
|
+
Subnets: props.privateSubnetIds,
|
|
219
|
+
SecurityGroups: [taskSg.GroupId],
|
|
220
|
+
AssignPublicIp: "DISABLED",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const networkConfig = new EcsService_NetworkConfiguration({
|
|
224
|
+
AwsvpcConfiguration: awsVpcConfig,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const service = new EcsService(
|
|
228
|
+
{
|
|
229
|
+
Cluster: cluster.Arn,
|
|
230
|
+
TaskDefinition: taskDef.TaskDefinitionArn,
|
|
231
|
+
LaunchType: "FARGATE",
|
|
232
|
+
DesiredCount: desiredCount,
|
|
233
|
+
HealthCheckGracePeriodSeconds: 60,
|
|
234
|
+
LoadBalancers: [serviceLoadBalancer],
|
|
235
|
+
NetworkConfiguration: networkConfig,
|
|
236
|
+
},
|
|
237
|
+
{ DependsOn: [listener] },
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
cluster,
|
|
242
|
+
executionRole,
|
|
243
|
+
taskRole,
|
|
244
|
+
logGroup,
|
|
245
|
+
taskDef,
|
|
246
|
+
albSg,
|
|
247
|
+
taskSg,
|
|
248
|
+
alb,
|
|
249
|
+
targetGroup,
|
|
250
|
+
listener,
|
|
251
|
+
service,
|
|
252
|
+
};
|
|
253
|
+
}, "FargateAlb");
|