@intentius/chant-lexicon-aws 0.0.6 → 0.0.9
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 +9444 -4597
- package/dist/rules/cf-refs.ts +99 -0
- package/dist/rules/ext001.ts +32 -25
- package/dist/rules/hardcoded-region.ts +1 -0
- package/dist/rules/iam-wildcard.ts +1 -0
- package/dist/rules/s3-encryption.ts +3 -3
- 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 +430 -0
- package/dist/types/index.d.ts +58525 -58501
- 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 +20 -20
- package/src/codegen/docs-links.test.ts +143 -0
- package/src/codegen/docs.ts +294 -124
- package/src/codegen/generate-lexicon.ts +8 -0
- package/src/codegen/generate-typescript.ts +25 -1
- package/src/codegen/generate.ts +1 -13
- package/src/codegen/package.ts +2 -0
- package/src/codegen/typecheck.test.ts +1 -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 +58525 -58501
- package/src/generated/index.ts +1351 -1351
- package/src/generated/lexicon-aws.json +9444 -4597
- package/src/import/generator.test.ts +5 -5
- package/src/import/generator.ts +4 -4
- package/src/import/roundtrip-fixtures.test.ts +2 -1
- package/src/import/roundtrip.test.ts +5 -5
- package/src/index.ts +21 -0
- package/src/integration.test.ts +92 -21
- 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 +32 -25
- 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/rules.test.ts +8 -8
- package/src/lint/rules/s3-encryption.ts +3 -3
- package/src/lsp/completions.ts +2 -0
- package/src/lsp/hover.ts +17 -0
- package/src/nested-stack-integration.test.ts +100 -0
- package/src/nested-stack.ts +2 -2
- package/src/plugin.test.ts +13 -15
- package/src/plugin.ts +552 -114
- package/src/serializer.test.ts +370 -43
- package/src/serializer.ts +69 -17
- 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
- package/dist/skills/aws-cloudformation.md +0 -41
- package/src/codegen/rollback.test.ts +0 -80
- package/src/codegen/rollback.ts +0 -20
package/src/serializer.test.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { createResource } from "@intentius/chant/runtime";
|
|
|
11
11
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
12
12
|
import type { BuildResult } from "@intentius/chant/build";
|
|
13
13
|
import { Parameter } from "./parameter";
|
|
14
|
+
import { defaultTags } from "./default-tags";
|
|
14
15
|
|
|
15
16
|
// Mock S3 Bucket for testing
|
|
16
17
|
class MockBucket implements Declarable {
|
|
@@ -18,9 +19,9 @@ class MockBucket implements Declarable {
|
|
|
18
19
|
readonly lexicon = "aws";
|
|
19
20
|
readonly entityType = "AWS::S3::Bucket";
|
|
20
21
|
readonly arn: AttrRef;
|
|
21
|
-
readonly props:
|
|
22
|
+
readonly props: Record<string, unknown>;
|
|
22
23
|
|
|
23
|
-
constructor(props: {
|
|
24
|
+
constructor(props: { BucketName?: string; VersioningConfiguration?: { Status: string }; Tags?: unknown[] } = {}) {
|
|
24
25
|
this.props = props;
|
|
25
26
|
this.arn = new AttrRef(this, "Arn");
|
|
26
27
|
}
|
|
@@ -57,7 +58,7 @@ describe("awsSerializer.serialize", () => {
|
|
|
57
58
|
|
|
58
59
|
test("serializes resources", () => {
|
|
59
60
|
const entities = new Map<string, Declarable>();
|
|
60
|
-
entities.set("MyBucket", new MockBucket({
|
|
61
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "my-bucket" }));
|
|
61
62
|
|
|
62
63
|
const output = awsSerializer.serialize(entities);
|
|
63
64
|
const template = JSON.parse(output);
|
|
@@ -86,8 +87,8 @@ describe("awsSerializer.serialize", () => {
|
|
|
86
87
|
test("serializes nested properties", () => {
|
|
87
88
|
const entities = new Map<string, Declarable>();
|
|
88
89
|
entities.set("MyBucket", new MockBucket({
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
BucketName: "my-bucket",
|
|
91
|
+
VersioningConfiguration: { Status: "Enabled" },
|
|
91
92
|
}));
|
|
92
93
|
|
|
93
94
|
const output = awsSerializer.serialize(entities);
|
|
@@ -98,21 +99,20 @@ describe("awsSerializer.serialize", () => {
|
|
|
98
99
|
});
|
|
99
100
|
});
|
|
100
101
|
|
|
101
|
-
test("
|
|
102
|
+
test("passes through property names verbatim", () => {
|
|
102
103
|
const entities = new Map<string, Declarable>();
|
|
103
|
-
entities.set("MyBucket", new MockBucket({
|
|
104
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "test" }));
|
|
104
105
|
|
|
105
106
|
const output = awsSerializer.serialize(entities);
|
|
106
107
|
const template = JSON.parse(output);
|
|
107
108
|
|
|
108
109
|
expect(template.Resources.MyBucket.Properties.BucketName).toBeDefined();
|
|
109
|
-
expect(template.Resources.MyBucket.Properties.bucketName).toBeUndefined();
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
test("handles multiple resources", () => {
|
|
113
113
|
const entities = new Map<string, Declarable>();
|
|
114
|
-
entities.set("DataBucket", new MockBucket({
|
|
115
|
-
entities.set("LogsBucket", new MockBucket({
|
|
114
|
+
entities.set("DataBucket", new MockBucket({ BucketName: "data-bucket" }));
|
|
115
|
+
entities.set("LogsBucket", new MockBucket({ BucketName: "logs-bucket" }));
|
|
116
116
|
|
|
117
117
|
const output = awsSerializer.serialize(entities);
|
|
118
118
|
const template = JSON.parse(output);
|
|
@@ -125,7 +125,7 @@ describe("awsSerializer.serialize", () => {
|
|
|
125
125
|
test("handles resources and parameters together", () => {
|
|
126
126
|
const entities = new Map<string, Declarable>();
|
|
127
127
|
entities.set("Env", new Parameter("String"));
|
|
128
|
-
entities.set("MyBucket", new MockBucket({
|
|
128
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
129
129
|
|
|
130
130
|
const output = awsSerializer.serialize(entities);
|
|
131
131
|
const template = JSON.parse(output);
|
|
@@ -154,9 +154,9 @@ class MockEncryption implements Declarable {
|
|
|
154
154
|
readonly lexicon = "aws";
|
|
155
155
|
readonly entityType = "AWS::S3::Bucket.BucketEncryption";
|
|
156
156
|
readonly kind = "property" as const;
|
|
157
|
-
readonly props: {
|
|
157
|
+
readonly props: { ServerSideEncryptionConfiguration: unknown[] };
|
|
158
158
|
|
|
159
|
-
constructor(props: {
|
|
159
|
+
constructor(props: { ServerSideEncryptionConfiguration: unknown[] }) {
|
|
160
160
|
this.props = props;
|
|
161
161
|
}
|
|
162
162
|
}
|
|
@@ -164,14 +164,14 @@ class MockEncryption implements Declarable {
|
|
|
164
164
|
describe("property-kind Declarables", () => {
|
|
165
165
|
test("property-kind Declarables are inlined into parent properties", () => {
|
|
166
166
|
const encryption = new MockEncryption({
|
|
167
|
-
|
|
168
|
-
{
|
|
167
|
+
ServerSideEncryptionConfiguration: [
|
|
168
|
+
{ ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } },
|
|
169
169
|
],
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
-
const bucket = new MockBucket({
|
|
172
|
+
const bucket = new MockBucket({ BucketName: "my-bucket" });
|
|
173
173
|
// Manually set encryption as a prop
|
|
174
|
-
(bucket.props as Record<string, unknown>).
|
|
174
|
+
(bucket.props as Record<string, unknown>).BucketEncryption = encryption;
|
|
175
175
|
|
|
176
176
|
const entities = new Map<string, Declarable>();
|
|
177
177
|
entities.set("DataEncryption", encryption);
|
|
@@ -181,23 +181,23 @@ describe("property-kind Declarables", () => {
|
|
|
181
181
|
const template = JSON.parse(output);
|
|
182
182
|
|
|
183
183
|
// Encryption should be inlined, not a Ref
|
|
184
|
-
expect(template.Resources.MyBucket.Properties.
|
|
184
|
+
expect(template.Resources.MyBucket.Properties.BucketEncryption).toEqual({
|
|
185
185
|
ServerSideEncryptionConfiguration: [
|
|
186
|
-
{ ServerSideEncryptionByDefault: {
|
|
186
|
+
{ ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } },
|
|
187
187
|
],
|
|
188
188
|
});
|
|
189
189
|
});
|
|
190
190
|
|
|
191
191
|
test("property-kind Declarables do NOT appear as standalone Resources", () => {
|
|
192
192
|
const encryption = new MockEncryption({
|
|
193
|
-
|
|
194
|
-
{
|
|
193
|
+
ServerSideEncryptionConfiguration: [
|
|
194
|
+
{ ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } },
|
|
195
195
|
],
|
|
196
196
|
});
|
|
197
197
|
|
|
198
198
|
const entities = new Map<string, Declarable>();
|
|
199
199
|
entities.set("DataEncryption", encryption);
|
|
200
|
-
entities.set("MyBucket", new MockBucket({
|
|
200
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "my-bucket" }));
|
|
201
201
|
|
|
202
202
|
const output = awsSerializer.serialize(entities);
|
|
203
203
|
const template = JSON.parse(output);
|
|
@@ -207,16 +207,16 @@ describe("property-kind Declarables", () => {
|
|
|
207
207
|
});
|
|
208
208
|
|
|
209
209
|
test("resource-kind Declarables still emit Ref when referenced", () => {
|
|
210
|
-
const sourceBucket = new MockBucket({
|
|
210
|
+
const sourceBucket = new MockBucket({ BucketName: "source" });
|
|
211
211
|
|
|
212
212
|
class MockConfig implements Declarable {
|
|
213
213
|
readonly [DECLARABLE_MARKER] = true as const;
|
|
214
214
|
readonly lexicon = "aws";
|
|
215
215
|
readonly entityType = "AWS::S3::ReplicationDestination";
|
|
216
|
-
readonly props: {
|
|
216
|
+
readonly props: { Bucket: Declarable };
|
|
217
217
|
|
|
218
|
-
constructor(
|
|
219
|
-
this.props = {
|
|
218
|
+
constructor(Bucket: Declarable) {
|
|
219
|
+
this.props = { Bucket };
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
@@ -233,7 +233,7 @@ describe("property-kind Declarables", () => {
|
|
|
233
233
|
|
|
234
234
|
describe("intrinsic serialization", () => {
|
|
235
235
|
test("handles AttrRef in properties", () => {
|
|
236
|
-
const source = new MockBucket({
|
|
236
|
+
const source = new MockBucket({ BucketName: "source" });
|
|
237
237
|
// Set the logical name on the AttrRef before using it
|
|
238
238
|
(source.arn as Record<string, unknown>)._setLogicalName("SourceBucket");
|
|
239
239
|
|
|
@@ -241,10 +241,10 @@ describe("intrinsic serialization", () => {
|
|
|
241
241
|
readonly [DECLARABLE_MARKER] = true as const;
|
|
242
242
|
readonly lexicon = "aws";
|
|
243
243
|
readonly entityType = "AWS::S3::ReplicationConfiguration";
|
|
244
|
-
readonly props: {
|
|
244
|
+
readonly props: { SourceArn: AttrRef };
|
|
245
245
|
|
|
246
|
-
constructor(
|
|
247
|
-
this.props = {
|
|
246
|
+
constructor(SourceArn: AttrRef) {
|
|
247
|
+
this.props = { SourceArn };
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
@@ -256,14 +256,14 @@ describe("intrinsic serialization", () => {
|
|
|
256
256
|
const template = JSON.parse(output);
|
|
257
257
|
|
|
258
258
|
expect(template.Resources.Replication.Properties.SourceArn).toEqual({
|
|
259
|
-
"Fn::
|
|
259
|
+
"Fn::GetAtt": ["SourceBucket", "Arn"],
|
|
260
260
|
});
|
|
261
261
|
});
|
|
262
262
|
});
|
|
263
263
|
|
|
264
264
|
describe("LexiconOutput serialization", () => {
|
|
265
265
|
test("generates CF Outputs section for LexiconOutputs", () => {
|
|
266
|
-
const bucket = new MockBucket({
|
|
266
|
+
const bucket = new MockBucket({ BucketName: "data-bucket" });
|
|
267
267
|
const lexiconOutput = new LexiconOutput(bucket.arn, "DataBucketArn");
|
|
268
268
|
lexiconOutput._setSourceEntity("dataBucket");
|
|
269
269
|
|
|
@@ -275,13 +275,13 @@ describe("LexiconOutput serialization", () => {
|
|
|
275
275
|
|
|
276
276
|
expect(template.Outputs).toBeDefined();
|
|
277
277
|
expect(template.Outputs.DataBucketArn).toEqual({
|
|
278
|
-
Value: { "Fn::
|
|
278
|
+
Value: { "Fn::GetAtt": ["dataBucket", "Arn"] },
|
|
279
279
|
});
|
|
280
280
|
});
|
|
281
281
|
|
|
282
282
|
test("generates multiple CF Outputs", () => {
|
|
283
|
-
const dataBucket = new MockBucket({
|
|
284
|
-
const logsBucket = new MockBucket({
|
|
283
|
+
const dataBucket = new MockBucket({ BucketName: "data-bucket" });
|
|
284
|
+
const logsBucket = new MockBucket({ BucketName: "logs-bucket" });
|
|
285
285
|
|
|
286
286
|
const dataOutput = new LexiconOutput(dataBucket.arn, "DataBucketArn");
|
|
287
287
|
dataOutput._setSourceEntity("dataBucket");
|
|
@@ -298,16 +298,16 @@ describe("LexiconOutput serialization", () => {
|
|
|
298
298
|
expect(template.Outputs).toBeDefined();
|
|
299
299
|
expect(Object.keys(template.Outputs)).toHaveLength(2);
|
|
300
300
|
expect(template.Outputs.DataBucketArn.Value).toEqual({
|
|
301
|
-
"Fn::
|
|
301
|
+
"Fn::GetAtt": ["dataBucket", "Arn"],
|
|
302
302
|
});
|
|
303
303
|
expect(template.Outputs.LogsBucketArn.Value).toEqual({
|
|
304
|
-
"Fn::
|
|
304
|
+
"Fn::GetAtt": ["logsBucket", "Arn"],
|
|
305
305
|
});
|
|
306
306
|
});
|
|
307
307
|
|
|
308
308
|
test("omits Outputs section when no LexiconOutputs provided", () => {
|
|
309
309
|
const entities = new Map<string, Declarable>();
|
|
310
|
-
entities.set("MyBucket", new MockBucket({
|
|
310
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
311
311
|
|
|
312
312
|
const result = awsSerializer.serialize(entities);
|
|
313
313
|
const template = JSON.parse(result);
|
|
@@ -317,7 +317,7 @@ describe("LexiconOutput serialization", () => {
|
|
|
317
317
|
|
|
318
318
|
test("omits Outputs section when empty LexiconOutputs array", () => {
|
|
319
319
|
const entities = new Map<string, Declarable>();
|
|
320
|
-
entities.set("MyBucket", new MockBucket({
|
|
320
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
321
321
|
|
|
322
322
|
const result = awsSerializer.serialize(entities, []);
|
|
323
323
|
const template = JSON.parse(result as string);
|
|
@@ -430,7 +430,7 @@ describe("nested stack serialization", () => {
|
|
|
430
430
|
Resources: {},
|
|
431
431
|
});
|
|
432
432
|
|
|
433
|
-
const bucket = new MockBucket({
|
|
433
|
+
const bucket = new MockBucket({ BucketName: "data" });
|
|
434
434
|
|
|
435
435
|
const entities = new Map<string, Declarable>();
|
|
436
436
|
entities.set("network", stack as unknown as Declarable);
|
|
@@ -446,7 +446,7 @@ describe("nested stack serialization", () => {
|
|
|
446
446
|
|
|
447
447
|
test("without nested stacks returns plain string", () => {
|
|
448
448
|
const entities = new Map<string, Declarable>();
|
|
449
|
-
entities.set("MyBucket", new MockBucket({
|
|
449
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
450
450
|
|
|
451
451
|
const result = awsSerializer.serialize(entities);
|
|
452
452
|
expect(typeof result).toBe("string");
|
|
@@ -460,7 +460,7 @@ describe("nested stack serialization", () => {
|
|
|
460
460
|
subnet: { Type: "AWS::EC2::Subnet" },
|
|
461
461
|
},
|
|
462
462
|
Outputs: {
|
|
463
|
-
subnetId: { Value: { "Fn::
|
|
463
|
+
subnetId: { Value: { "Fn::GetAtt": ["subnet", "SubnetId"] } },
|
|
464
464
|
},
|
|
465
465
|
});
|
|
466
466
|
|
|
@@ -472,7 +472,7 @@ describe("nested stack serialization", () => {
|
|
|
472
472
|
entityType: "AWS::Lambda::Function",
|
|
473
473
|
kind: "resource" as const,
|
|
474
474
|
props: {
|
|
475
|
-
|
|
475
|
+
VpcConfig: { SubnetIds: [subnetRef] },
|
|
476
476
|
},
|
|
477
477
|
} as unknown as Declarable;
|
|
478
478
|
|
|
@@ -490,3 +490,330 @@ describe("nested stack serialization", () => {
|
|
|
490
490
|
});
|
|
491
491
|
});
|
|
492
492
|
});
|
|
493
|
+
|
|
494
|
+
// ── Resource-Level CF Attributes ──────────────────────────
|
|
495
|
+
|
|
496
|
+
// Mock resource that supports the second constructor `attributes` argument
|
|
497
|
+
class MockResourceWithAttrs implements Declarable {
|
|
498
|
+
readonly [DECLARABLE_MARKER] = true as const;
|
|
499
|
+
readonly lexicon = "aws";
|
|
500
|
+
readonly entityType: string;
|
|
501
|
+
readonly props: Record<string, unknown>;
|
|
502
|
+
readonly attributes: Record<string, unknown>;
|
|
503
|
+
|
|
504
|
+
constructor(
|
|
505
|
+
type: string,
|
|
506
|
+
props: Record<string, unknown>,
|
|
507
|
+
attributes: Record<string, unknown> = {},
|
|
508
|
+
) {
|
|
509
|
+
this.entityType = type;
|
|
510
|
+
this.props = props;
|
|
511
|
+
Object.defineProperty(this, "attributes", { value: attributes, enumerable: false, configurable: true });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
describe("resource-level CF attributes", () => {
|
|
516
|
+
function serialize(...entries: [string, Declarable][]) {
|
|
517
|
+
const entities = new Map<string, Declarable>(entries);
|
|
518
|
+
const output = awsSerializer.serialize(entities);
|
|
519
|
+
return JSON.parse(output as string);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
test("DependsOn with string logical name", () => {
|
|
523
|
+
const res = new MockResourceWithAttrs(
|
|
524
|
+
"AWS::EC2::Instance", { InstanceType: "t3.micro" }, { DependsOn: "Database" },
|
|
525
|
+
);
|
|
526
|
+
const template = serialize(["Server", res]);
|
|
527
|
+
expect(template.Resources.Server.DependsOn).toBe("Database");
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("DependsOn with Declarable reference resolves to logical name", () => {
|
|
531
|
+
const bucket = new MockBucket({ BucketName: "data" });
|
|
532
|
+
const fn = new MockResourceWithAttrs(
|
|
533
|
+
"AWS::Lambda::Function", { Runtime: "nodejs20.x" }, { DependsOn: bucket },
|
|
534
|
+
);
|
|
535
|
+
const template = serialize(["DataBucket", bucket], ["Handler", fn]);
|
|
536
|
+
expect(template.Resources.Handler.DependsOn).toBe("DataBucket");
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test("DependsOn with array of mixed strings and Declarables", () => {
|
|
540
|
+
const bucket = new MockBucket({ BucketName: "data" });
|
|
541
|
+
const fn = new MockResourceWithAttrs(
|
|
542
|
+
"AWS::Lambda::Function", { Runtime: "nodejs20.x" },
|
|
543
|
+
{ DependsOn: [bucket, "ExternalResource"] },
|
|
544
|
+
);
|
|
545
|
+
const template = serialize(["DataBucket", bucket], ["Handler", fn]);
|
|
546
|
+
expect(template.Resources.Handler.DependsOn).toEqual(["DataBucket", "ExternalResource"]);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test("single DependsOn array item serializes as string, not array", () => {
|
|
550
|
+
const fn = new MockResourceWithAttrs(
|
|
551
|
+
"AWS::Lambda::Function", { Runtime: "nodejs20.x" }, { DependsOn: ["OnlyOne"] },
|
|
552
|
+
);
|
|
553
|
+
const template = serialize(["Handler", fn]);
|
|
554
|
+
expect(template.Resources.Handler.DependsOn).toBe("OnlyOne");
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
test("Condition attribute", () => {
|
|
558
|
+
const res = new MockResourceWithAttrs(
|
|
559
|
+
"AWS::S3::Bucket", { BucketName: "cond-bucket" }, { Condition: "CreateProdResources" },
|
|
560
|
+
);
|
|
561
|
+
const template = serialize(["MyBucket", res]);
|
|
562
|
+
expect(template.Resources.MyBucket.Condition).toBe("CreateProdResources");
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("DeletionPolicy attribute", () => {
|
|
566
|
+
const res = new MockResourceWithAttrs(
|
|
567
|
+
"AWS::RDS::DBInstance", { DBInstanceClass: "db.t3.micro" }, { DeletionPolicy: "Retain" },
|
|
568
|
+
);
|
|
569
|
+
const template = serialize(["Database", res]);
|
|
570
|
+
expect(template.Resources.Database.DeletionPolicy).toBe("Retain");
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test("UpdateReplacePolicy attribute", () => {
|
|
574
|
+
const res = new MockResourceWithAttrs(
|
|
575
|
+
"AWS::RDS::DBInstance", { DBInstanceClass: "db.t3.micro" }, { UpdateReplacePolicy: "Snapshot" },
|
|
576
|
+
);
|
|
577
|
+
const template = serialize(["Database", res]);
|
|
578
|
+
expect(template.Resources.Database.UpdateReplacePolicy).toBe("Snapshot");
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("UpdatePolicy attribute", () => {
|
|
582
|
+
const policy = {
|
|
583
|
+
AutoScalingRollingUpdate: {
|
|
584
|
+
MaxBatchSize: 2,
|
|
585
|
+
MinInstancesInService: 1,
|
|
586
|
+
PauseTime: "PT5M",
|
|
587
|
+
WaitOnResourceSignals: true,
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
const res = new MockResourceWithAttrs(
|
|
591
|
+
"AWS::AutoScaling::AutoScalingGroup", { MinSize: "1", MaxSize: "4" },
|
|
592
|
+
{ UpdatePolicy: policy },
|
|
593
|
+
);
|
|
594
|
+
const template = serialize(["ASG", res]);
|
|
595
|
+
expect(template.Resources.ASG.UpdatePolicy).toEqual(policy);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("CreationPolicy attribute", () => {
|
|
599
|
+
const policy = { ResourceSignal: { Count: 3, Timeout: "PT15M" } };
|
|
600
|
+
const res = new MockResourceWithAttrs(
|
|
601
|
+
"AWS::AutoScaling::AutoScalingGroup", { MinSize: "3", MaxSize: "3" },
|
|
602
|
+
{ CreationPolicy: policy },
|
|
603
|
+
);
|
|
604
|
+
const template = serialize(["ASG", res]);
|
|
605
|
+
expect(template.Resources.ASG.CreationPolicy).toEqual(policy);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
test("Metadata attribute with plain object", () => {
|
|
609
|
+
const res = new MockResourceWithAttrs(
|
|
610
|
+
"AWS::EC2::Instance", { InstanceType: "t3.micro" },
|
|
611
|
+
{ Metadata: { "AWS::CloudFormation::Init": { config: { packages: { yum: { httpd: [] } } } } } },
|
|
612
|
+
);
|
|
613
|
+
const template = serialize(["Server", res]);
|
|
614
|
+
expect(template.Resources.Server.Metadata).toEqual({
|
|
615
|
+
"AWS::CloudFormation::Init": { config: { packages: { yum: { httpd: [] } } } },
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
test("Metadata with intrinsic values resolves them", () => {
|
|
620
|
+
const res = new MockResourceWithAttrs(
|
|
621
|
+
"AWS::EC2::Instance", { InstanceType: "t3.micro" },
|
|
622
|
+
{ Metadata: { StackInfo: Sub`${AWS.StackName}-metadata` } },
|
|
623
|
+
);
|
|
624
|
+
const template = serialize(["Server", res]);
|
|
625
|
+
expect(template.Resources.Server.Metadata.StackInfo).toEqual({
|
|
626
|
+
"Fn::Sub": "${AWS::StackName}-metadata",
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
test("all 7 attributes on a single resource", () => {
|
|
631
|
+
const dependency = new MockBucket({ BucketName: "dep" });
|
|
632
|
+
const res = new MockResourceWithAttrs(
|
|
633
|
+
"AWS::RDS::DBInstance", { DBInstanceClass: "db.t3.micro", Engine: "postgres" },
|
|
634
|
+
{
|
|
635
|
+
DependsOn: dependency,
|
|
636
|
+
Condition: "CreateDatabase",
|
|
637
|
+
DeletionPolicy: "Snapshot",
|
|
638
|
+
UpdateReplacePolicy: "Retain",
|
|
639
|
+
UpdatePolicy: { AutoScalingReplacingUpdate: { WillReplace: true } },
|
|
640
|
+
CreationPolicy: { ResourceSignal: { Count: 1, Timeout: "PT10M" } },
|
|
641
|
+
Metadata: { Version: "1.0" },
|
|
642
|
+
},
|
|
643
|
+
);
|
|
644
|
+
const template = serialize(["DepBucket", dependency], ["Database", res]);
|
|
645
|
+
|
|
646
|
+
const db = template.Resources.Database;
|
|
647
|
+
expect(db.DependsOn).toBe("DepBucket");
|
|
648
|
+
expect(db.Condition).toBe("CreateDatabase");
|
|
649
|
+
expect(db.DeletionPolicy).toBe("Snapshot");
|
|
650
|
+
expect(db.UpdateReplacePolicy).toBe("Retain");
|
|
651
|
+
expect(db.UpdatePolicy).toEqual({ AutoScalingReplacingUpdate: { WillReplace: true } });
|
|
652
|
+
expect(db.CreationPolicy).toEqual({ ResourceSignal: { Count: 1, Timeout: "PT10M" } });
|
|
653
|
+
expect(db.Metadata).toEqual({ Version: "1.0" });
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("undefined attributes are omitted from CF template", () => {
|
|
657
|
+
const res = new MockResourceWithAttrs(
|
|
658
|
+
"AWS::S3::Bucket", { BucketName: "bucket" },
|
|
659
|
+
{ DependsOn: undefined, Condition: undefined },
|
|
660
|
+
);
|
|
661
|
+
const template = serialize(["MyBucket", res]);
|
|
662
|
+
expect(template.Resources.MyBucket.DependsOn).toBeUndefined();
|
|
663
|
+
expect(template.Resources.MyBucket.Condition).toBeUndefined();
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test("empty attributes object produces no resource-level attributes", () => {
|
|
667
|
+
const res = new MockResourceWithAttrs("AWS::S3::Bucket", { BucketName: "bucket" }, {});
|
|
668
|
+
const template = serialize(["MyBucket", res]);
|
|
669
|
+
const r = template.Resources.MyBucket;
|
|
670
|
+
expect(r.DependsOn).toBeUndefined();
|
|
671
|
+
expect(r.Condition).toBeUndefined();
|
|
672
|
+
expect(r.DeletionPolicy).toBeUndefined();
|
|
673
|
+
expect(r.Metadata).toBeUndefined();
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
test("resource without attributes property works unchanged", () => {
|
|
677
|
+
// MockBucket has no `attributes` property — should still serialize fine
|
|
678
|
+
const bucket = new MockBucket({ BucketName: "legacy" });
|
|
679
|
+
const template = serialize(["MyBucket", bucket]);
|
|
680
|
+
expect(template.Resources.MyBucket.Type).toBe("AWS::S3::Bucket");
|
|
681
|
+
expect(template.Resources.MyBucket.Properties.BucketName).toBe("legacy");
|
|
682
|
+
expect(template.Resources.MyBucket.DependsOn).toBeUndefined();
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// ── Default Tags Serialization ──────────────────────────
|
|
687
|
+
|
|
688
|
+
// Mock Lambda Permission (non-taggable) for testing
|
|
689
|
+
class MockPermission implements Declarable {
|
|
690
|
+
readonly [DECLARABLE_MARKER] = true as const;
|
|
691
|
+
readonly lexicon = "aws";
|
|
692
|
+
readonly entityType = "AWS::Lambda::Permission";
|
|
693
|
+
readonly props: { Action: string; FunctionName: string; Principal: string };
|
|
694
|
+
|
|
695
|
+
constructor(props: { Action: string; FunctionName: string; Principal: string }) {
|
|
696
|
+
this.props = props;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Mock Lambda Function (taggable) for testing
|
|
701
|
+
class MockFunction implements Declarable {
|
|
702
|
+
readonly [DECLARABLE_MARKER] = true as const;
|
|
703
|
+
readonly lexicon = "aws";
|
|
704
|
+
readonly entityType = "AWS::Lambda::Function";
|
|
705
|
+
readonly props: Record<string, unknown>;
|
|
706
|
+
|
|
707
|
+
constructor(props: Record<string, unknown> = {}) {
|
|
708
|
+
this.props = props;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
describe("default tags serialization", () => {
|
|
713
|
+
test("DefaultTags entity is not emitted as a CF Resource", () => {
|
|
714
|
+
const entities = new Map<string, Declarable>();
|
|
715
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
716
|
+
entities.set("tags", defaultTags([{ Key: "Env", Value: "prod" }]) as unknown as Declarable);
|
|
717
|
+
|
|
718
|
+
const output = awsSerializer.serialize(entities);
|
|
719
|
+
const template = JSON.parse(output as string);
|
|
720
|
+
|
|
721
|
+
expect(template.Resources.tags).toBeUndefined();
|
|
722
|
+
expect(template.Resources.MyBucket).toBeDefined();
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
test("taggable resource gets default tags injected", () => {
|
|
726
|
+
const entities = new Map<string, Declarable>();
|
|
727
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
728
|
+
entities.set("tags", defaultTags([{ Key: "Env", Value: "prod" }]) as unknown as Declarable);
|
|
729
|
+
|
|
730
|
+
const output = awsSerializer.serialize(entities);
|
|
731
|
+
const template = JSON.parse(output as string);
|
|
732
|
+
|
|
733
|
+
expect(template.Resources.MyBucket.Properties.Tags).toEqual([
|
|
734
|
+
{ Key: "Env", Value: "prod" },
|
|
735
|
+
]);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
test("non-taggable resource does NOT get tags", () => {
|
|
739
|
+
const entities = new Map<string, Declarable>();
|
|
740
|
+
entities.set("Perm", new MockPermission({
|
|
741
|
+
Action: "lambda:InvokeFunction",
|
|
742
|
+
FunctionName: "fn",
|
|
743
|
+
Principal: "apigateway.amazonaws.com",
|
|
744
|
+
}));
|
|
745
|
+
entities.set("tags", defaultTags([{ Key: "Env", Value: "prod" }]) as unknown as Declarable);
|
|
746
|
+
|
|
747
|
+
const output = awsSerializer.serialize(entities);
|
|
748
|
+
const template = JSON.parse(output as string);
|
|
749
|
+
|
|
750
|
+
expect(template.Resources.Perm.Properties.Tags).toBeUndefined();
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test("explicit tags win over defaults on same key", () => {
|
|
754
|
+
const entities = new Map<string, Declarable>();
|
|
755
|
+
entities.set("MyBucket", new MockBucket({
|
|
756
|
+
BucketName: "bucket",
|
|
757
|
+
Tags: [{ Key: "Env", Value: "staging" }],
|
|
758
|
+
}));
|
|
759
|
+
entities.set("tags", defaultTags([
|
|
760
|
+
{ Key: "Env", Value: "prod" },
|
|
761
|
+
{ Key: "Team", Value: "platform" },
|
|
762
|
+
]) as unknown as Declarable);
|
|
763
|
+
|
|
764
|
+
const output = awsSerializer.serialize(entities);
|
|
765
|
+
const template = JSON.parse(output as string);
|
|
766
|
+
|
|
767
|
+
const tags = template.Resources.MyBucket.Properties.Tags;
|
|
768
|
+
expect(tags).toHaveLength(2);
|
|
769
|
+
// Explicit "Env" wins, default "Team" is added
|
|
770
|
+
const envTag = tags.find((t: { Key: string }) => t.Key === "Env");
|
|
771
|
+
const teamTag = tags.find((t: { Key: string }) => t.Key === "Team");
|
|
772
|
+
expect(envTag.Value).toBe("staging");
|
|
773
|
+
expect(teamTag.Value).toBe("platform");
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
test("intrinsic tag values resolve correctly", () => {
|
|
777
|
+
const entities = new Map<string, Declarable>();
|
|
778
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
779
|
+
entities.set("tags", defaultTags([
|
|
780
|
+
{ Key: "Stack", Value: Sub`${AWS.StackName}` },
|
|
781
|
+
]) as unknown as Declarable);
|
|
782
|
+
|
|
783
|
+
const output = awsSerializer.serialize(entities);
|
|
784
|
+
const template = JSON.parse(output as string);
|
|
785
|
+
|
|
786
|
+
const tags = template.Resources.MyBucket.Properties.Tags;
|
|
787
|
+
expect(tags).toHaveLength(1);
|
|
788
|
+
expect(tags[0].Key).toBe("Stack");
|
|
789
|
+
expect(tags[0].Value).toEqual({ "Fn::Sub": "${AWS::StackName}" });
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
test("parameter tag values resolve to Ref", () => {
|
|
793
|
+
const env = new Parameter("String", { defaultValue: "dev" });
|
|
794
|
+
const entities = new Map<string, Declarable>();
|
|
795
|
+
entities.set("Env", env as unknown as Declarable);
|
|
796
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
797
|
+
entities.set("tags", defaultTags([
|
|
798
|
+
{ Key: "Environment", Value: env },
|
|
799
|
+
]) as unknown as Declarable);
|
|
800
|
+
|
|
801
|
+
const output = awsSerializer.serialize(entities);
|
|
802
|
+
const template = JSON.parse(output as string);
|
|
803
|
+
|
|
804
|
+
const tags = template.Resources.MyBucket.Properties.Tags;
|
|
805
|
+
expect(tags).toHaveLength(1);
|
|
806
|
+
expect(tags[0].Key).toBe("Environment");
|
|
807
|
+
expect(tags[0].Value).toEqual({ Ref: "Env" });
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
test("no defaultTags = no injection (existing behavior)", () => {
|
|
811
|
+
const entities = new Map<string, Declarable>();
|
|
812
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
813
|
+
|
|
814
|
+
const output = awsSerializer.serialize(entities);
|
|
815
|
+
const template = JSON.parse(output as string);
|
|
816
|
+
|
|
817
|
+
expect(template.Resources.MyBucket.Properties.Tags).toBeUndefined();
|
|
818
|
+
});
|
|
819
|
+
});
|