@intentius/chant-lexicon-aws 0.0.6 → 0.0.8
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 +7 -7
- package/dist/manifest.json +1 -1
- package/dist/meta.json +3701 -3701
- package/dist/rules/ext001.ts +2 -4
- package/dist/rules/s3-encryption.ts +2 -3
- package/dist/skills/chant-aws.md +72 -0
- package/dist/types/index.d.ts +57087 -57087
- package/package.json +1 -1
- package/src/codegen/__snapshots__/snapshot.test.ts.snap +18 -18
- package/src/codegen/docs.ts +59 -4
- package/src/codegen/generate.ts +1 -13
- package/src/codegen/package.ts +2 -0
- package/src/codegen/typecheck.test.ts +1 -1
- package/src/generated/index.d.ts +57087 -57087
- package/src/generated/index.ts +1351 -1351
- package/src/generated/lexicon-aws.json +3701 -3701
- 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/integration.test.ts +21 -21
- package/src/lint/post-synth/ext001.ts +2 -4
- package/src/lint/rules/rules.test.ts +8 -8
- package/src/lint/rules/s3-encryption.ts +2 -3
- package/src/lsp/completions.ts +2 -0
- package/src/lsp/hover.ts +2 -0
- package/src/nested-stack.ts +1 -1
- package/src/plugin.test.ts +13 -15
- package/src/plugin.ts +116 -110
- package/src/serializer.test.ts +42 -43
- package/src/serializer.ts +7 -16
- 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/plugin.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
1
2
|
import type { LexiconPlugin, IntrinsicDef, SkillDefinition } from "@intentius/chant/lexicon";
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
2
4
|
import type { LintRule } from "@intentius/chant/lint/rule";
|
|
3
5
|
import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
|
|
4
6
|
import type { TemplateParser } from "@intentius/chant/import/parser";
|
|
@@ -58,34 +60,34 @@ export const awsPlugin: LexiconPlugin = {
|
|
|
58
60
|
* Shared bucket configuration — encryption, versioning, public access
|
|
59
61
|
*/
|
|
60
62
|
|
|
61
|
-
import
|
|
63
|
+
import { ServerSideEncryptionByDefault, ServerSideEncryptionRule, BucketEncryption, PublicAccessBlockConfiguration, VersioningConfiguration } from "@intentius/chant-lexicon-aws";
|
|
62
64
|
|
|
63
65
|
// Encryption default — AES256 server-side encryption
|
|
64
|
-
export const encryptionDefault = new
|
|
65
|
-
|
|
66
|
+
export const encryptionDefault = new ServerSideEncryptionByDefault({
|
|
67
|
+
SSEAlgorithm: "AES256",
|
|
66
68
|
});
|
|
67
69
|
|
|
68
70
|
// Encryption rule wrapping the default
|
|
69
|
-
export const encryptionRule = new
|
|
70
|
-
|
|
71
|
+
export const encryptionRule = new ServerSideEncryptionRule({
|
|
72
|
+
ServerSideEncryptionByDefault: encryptionDefault,
|
|
71
73
|
});
|
|
72
74
|
|
|
73
75
|
// Bucket encryption configuration
|
|
74
|
-
export const bucketEncryption = new
|
|
75
|
-
|
|
76
|
+
export const bucketEncryption = new BucketEncryption({
|
|
77
|
+
ServerSideEncryptionConfiguration: [encryptionRule],
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
// Public access block — deny all public access
|
|
79
|
-
export const publicAccessBlock = new
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
export const publicAccessBlock = new PublicAccessBlockConfiguration({
|
|
82
|
+
BlockPublicAcls: true,
|
|
83
|
+
BlockPublicPolicy: true,
|
|
84
|
+
IgnorePublicAcls: true,
|
|
85
|
+
RestrictPublicBuckets: true,
|
|
84
86
|
});
|
|
85
87
|
|
|
86
88
|
// Versioning — enabled
|
|
87
|
-
export const versioningEnabled = new
|
|
88
|
-
|
|
89
|
+
export const versioningEnabled = new VersioningConfiguration({
|
|
90
|
+
Status: "Enabled",
|
|
89
91
|
});
|
|
90
92
|
`,
|
|
91
93
|
"data-bucket.ts": `/**
|
|
@@ -96,10 +98,10 @@ import { Bucket, Sub, AWS } from "@intentius/chant-lexicon-aws";
|
|
|
96
98
|
import { versioningEnabled, bucketEncryption, publicAccessBlock } from "./config";
|
|
97
99
|
|
|
98
100
|
export const dataBucket = new Bucket({
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
BucketName: Sub\`\${AWS.StackName}-\${AWS.AccountId}-data\`,
|
|
102
|
+
VersioningConfiguration: versioningEnabled,
|
|
103
|
+
BucketEncryption: bucketEncryption,
|
|
104
|
+
PublicAccessBlockConfiguration: publicAccessBlock,
|
|
103
105
|
});
|
|
104
106
|
`,
|
|
105
107
|
"logs-bucket.ts": `/**
|
|
@@ -110,11 +112,11 @@ import { Bucket, Sub, AWS } from "@intentius/chant-lexicon-aws";
|
|
|
110
112
|
import { versioningEnabled, bucketEncryption, publicAccessBlock } from "./config";
|
|
111
113
|
|
|
112
114
|
export const logsBucket = new Bucket({
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
BucketName: Sub\`\${AWS.StackName}-\${AWS.AccountId}-logs\`,
|
|
116
|
+
AccessControl: "LogDeliveryWrite",
|
|
117
|
+
VersioningConfiguration: versioningEnabled,
|
|
118
|
+
BucketEncryption: bucketEncryption,
|
|
119
|
+
PublicAccessBlockConfiguration: publicAccessBlock,
|
|
118
120
|
});
|
|
119
121
|
`,
|
|
120
122
|
};
|
|
@@ -229,44 +231,17 @@ export const logsBucket = new Bucket({
|
|
|
229
231
|
|
|
230
232
|
console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
231
233
|
|
|
232
|
-
// Produce .tgz via
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const packErr = await new Response(packProc.stderr).text();
|
|
240
|
-
const packExit = await packProc.exited;
|
|
234
|
+
// Produce .tgz via pack command
|
|
235
|
+
const { getRuntime } = await import("@intentius/chant/runtime-adapter");
|
|
236
|
+
const rt = getRuntime();
|
|
237
|
+
const { stdout: packOut, stderr: packErr, exitCode: packExit } = await rt.spawn(
|
|
238
|
+
rt.commands.packCmd,
|
|
239
|
+
{ cwd: pkgDir },
|
|
240
|
+
);
|
|
241
241
|
if (packExit === 0) {
|
|
242
242
|
console.error(`Tarball: ${packOut.trim()}`);
|
|
243
243
|
} else {
|
|
244
|
-
console.error(
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
|
|
248
|
-
async rollback(options?: { restore?: string; verbose?: boolean }): Promise<void> {
|
|
249
|
-
const { listSnapshots, restoreSnapshot } = await import("./codegen/rollback");
|
|
250
|
-
const { join, dirname } = await import("path");
|
|
251
|
-
const { fileURLToPath } = await import("url");
|
|
252
|
-
|
|
253
|
-
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
254
|
-
const snapshotsDir = join(pkgDir, ".snapshots");
|
|
255
|
-
|
|
256
|
-
if (options?.restore) {
|
|
257
|
-
const generatedDir = join(pkgDir, "src", "generated");
|
|
258
|
-
restoreSnapshot(String(options.restore), generatedDir);
|
|
259
|
-
console.error(`Restored snapshot: ${options.restore}`);
|
|
260
|
-
} else {
|
|
261
|
-
const snapshots = listSnapshots(snapshotsDir);
|
|
262
|
-
if (snapshots.length === 0) {
|
|
263
|
-
console.error("No snapshots available.");
|
|
264
|
-
} else {
|
|
265
|
-
console.error(`Available snapshots (${snapshots.length}):`);
|
|
266
|
-
for (const s of snapshots) {
|
|
267
|
-
console.error(` ${s.timestamp} ${s.resourceCount} resources ${s.path}`);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
244
|
+
console.error(`${rt.commands.packCmd.join(" ")} failed: ${packErr}`);
|
|
270
245
|
}
|
|
271
246
|
},
|
|
272
247
|
|
|
@@ -278,49 +253,80 @@ export const logsBucket = new Bucket({
|
|
|
278
253
|
skills(): SkillDefinition[] {
|
|
279
254
|
return [
|
|
280
255
|
{
|
|
281
|
-
name: "aws
|
|
282
|
-
description: "AWS CloudFormation
|
|
256
|
+
name: "chant-aws",
|
|
257
|
+
description: "AWS CloudFormation template management — workflows, patterns, and troubleshooting",
|
|
283
258
|
content: `---
|
|
284
|
-
|
|
285
|
-
description:
|
|
259
|
+
skill: chant-aws
|
|
260
|
+
description: Build, validate, and deploy CloudFormation templates from a chant project
|
|
261
|
+
user-invocable: true
|
|
286
262
|
---
|
|
287
263
|
|
|
288
|
-
#
|
|
264
|
+
# Deploying CloudFormation from Chant
|
|
289
265
|
|
|
290
|
-
|
|
266
|
+
This project defines CloudFormation resources as TypeScript in \`src/\`. Use these steps to build, validate, and deploy.
|
|
291
267
|
|
|
292
|
-
|
|
293
|
-
- \`AWS::Lambda::Function\` — Serverless compute
|
|
294
|
-
- \`AWS::DynamoDB::Table\` — NoSQL database
|
|
295
|
-
- \`AWS::IAM::Role\` — Identity and access management
|
|
296
|
-
- \`AWS::SNS::Topic\` — Pub/sub messaging
|
|
297
|
-
- \`AWS::SQS::Queue\` — Message queue
|
|
298
|
-
- \`AWS::EC2::SecurityGroup\` — Network firewall rules
|
|
268
|
+
## Build the template
|
|
299
269
|
|
|
300
|
-
|
|
270
|
+
\`\`\`bash
|
|
271
|
+
chant build src/ --output stack.json
|
|
272
|
+
\`\`\`
|
|
301
273
|
|
|
302
|
-
|
|
303
|
-
- \`Ref\` — Reference a resource or parameter
|
|
304
|
-
- \`GetAtt\` — Get a resource attribute (e.g. ARN)
|
|
305
|
-
- \`If\` — Conditional value based on a condition
|
|
306
|
-
- \`Join\` — Join strings with a delimiter
|
|
307
|
-
- \`Select\` — Pick an item from a list by index
|
|
274
|
+
## Validate before deploying
|
|
308
275
|
|
|
309
|
-
|
|
276
|
+
\`\`\`bash
|
|
277
|
+
chant lint src/
|
|
278
|
+
aws cloudformation validate-template --template-body file://stack.json
|
|
279
|
+
\`\`\`
|
|
310
280
|
|
|
311
|
-
|
|
312
|
-
- \`AWS::Region\` — Current deployment region
|
|
313
|
-
- \`AWS::AccountId\` — Current AWS account ID
|
|
314
|
-
- \`AWS::Partition\` — Partition (aws, aws-cn, aws-us-gov)
|
|
281
|
+
## Deploy a new stack
|
|
315
282
|
|
|
316
|
-
|
|
283
|
+
\`\`\`bash
|
|
284
|
+
aws cloudformation deploy \\
|
|
285
|
+
--template-file stack.json \\
|
|
286
|
+
--stack-name <stack-name> \\
|
|
287
|
+
--capabilities CAPABILITY_NAMED_IAM
|
|
288
|
+
\`\`\`
|
|
317
289
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
290
|
+
Add \`--parameter-overrides Key=Value\` if the template has parameters.
|
|
291
|
+
|
|
292
|
+
## Update an existing stack
|
|
293
|
+
|
|
294
|
+
1. Edit the TypeScript source
|
|
295
|
+
2. Rebuild: \`chant build src/ --output stack.json\`
|
|
296
|
+
3. Preview changes:
|
|
297
|
+
\`\`\`bash
|
|
298
|
+
aws cloudformation create-change-set \\
|
|
299
|
+
--stack-name <stack-name> \\
|
|
300
|
+
--template-body file://stack.json \\
|
|
301
|
+
--change-set-name update-$(date +%s) \\
|
|
302
|
+
--capabilities CAPABILITY_NAMED_IAM
|
|
303
|
+
aws cloudformation describe-change-set \\
|
|
304
|
+
--stack-name <stack-name> \\
|
|
305
|
+
--change-set-name update-<id>
|
|
306
|
+
\`\`\`
|
|
307
|
+
4. Execute: \`aws cloudformation execute-change-set --stack-name <stack-name> --change-set-name update-<id>\`
|
|
308
|
+
|
|
309
|
+
Or deploy directly: \`aws cloudformation deploy --template-file stack.json --stack-name <stack-name> --capabilities CAPABILITY_NAMED_IAM\`
|
|
310
|
+
|
|
311
|
+
## Delete a stack
|
|
312
|
+
|
|
313
|
+
\`\`\`bash
|
|
314
|
+
aws cloudformation delete-stack --stack-name <stack-name>
|
|
315
|
+
aws cloudformation wait stack-delete-complete --stack-name <stack-name>
|
|
316
|
+
\`\`\`
|
|
317
|
+
|
|
318
|
+
## Check stack status
|
|
319
|
+
|
|
320
|
+
\`\`\`bash
|
|
321
|
+
aws cloudformation describe-stacks --stack-name <stack-name>
|
|
322
|
+
aws cloudformation describe-stack-events --stack-name <stack-name> --max-items 10
|
|
323
|
+
\`\`\`
|
|
324
|
+
|
|
325
|
+
## Troubleshooting deploy failures
|
|
326
|
+
|
|
327
|
+
- Check events: \`aws cloudformation describe-stack-events --stack-name <stack-name>\`
|
|
328
|
+
- Rollback stuck: \`aws cloudformation continue-update-rollback --stack-name <stack-name>\`
|
|
329
|
+
- Drift: \`aws cloudformation detect-stack-drift --stack-name <stack-name>\`
|
|
324
330
|
`,
|
|
325
331
|
triggers: [
|
|
326
332
|
{ type: "file-pattern", value: "**/*.aws.ts" },
|
|
@@ -424,31 +430,31 @@ description: AWS CloudFormation best practices and common patterns
|
|
|
424
430
|
description: "AWS S3 bucket with versioning and encryption",
|
|
425
431
|
mimeType: "text/typescript",
|
|
426
432
|
async handler(): Promise<string> {
|
|
427
|
-
return `import
|
|
433
|
+
return `import { ServerSideEncryptionByDefault, ServerSideEncryptionRule, BucketEncryption, VersioningConfiguration, Bucket, Sub, AWS } from "@intentius/chant-lexicon-aws";
|
|
428
434
|
|
|
429
435
|
// Encryption configuration
|
|
430
|
-
export const encryptionDefault = new
|
|
431
|
-
|
|
436
|
+
export const encryptionDefault = new ServerSideEncryptionByDefault({
|
|
437
|
+
SSEAlgorithm: "AES256",
|
|
432
438
|
});
|
|
433
439
|
|
|
434
|
-
export const encryptionRule = new
|
|
435
|
-
|
|
440
|
+
export const encryptionRule = new ServerSideEncryptionRule({
|
|
441
|
+
ServerSideEncryptionByDefault: encryptionDefault,
|
|
436
442
|
});
|
|
437
443
|
|
|
438
|
-
export const bucketEncryption = new
|
|
439
|
-
|
|
444
|
+
export const bucketEncryption = new BucketEncryption({
|
|
445
|
+
ServerSideEncryptionConfiguration: [encryptionRule],
|
|
440
446
|
});
|
|
441
447
|
|
|
442
448
|
// Versioning
|
|
443
|
-
export const versioningEnabled = new
|
|
444
|
-
|
|
449
|
+
export const versioningEnabled = new VersioningConfiguration({
|
|
450
|
+
Status: "Enabled",
|
|
445
451
|
});
|
|
446
452
|
|
|
447
|
-
// Create a versioned bucket with encryption
|
|
448
|
-
export const dataBucket = new
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
453
|
+
// Create a versioned bucket with encryption (AccountId ensures global uniqueness)
|
|
454
|
+
export const dataBucket = new Bucket({
|
|
455
|
+
BucketName: Sub\`\${AWS.StackName}-\${AWS.AccountId}-data\`,
|
|
456
|
+
VersioningConfiguration: versioningEnabled,
|
|
457
|
+
BucketEncryption: bucketEncryption,
|
|
452
458
|
});
|
|
453
459
|
`;
|
|
454
460
|
},
|
|
@@ -459,17 +465,17 @@ export const dataBucket = new aws.Bucket({
|
|
|
459
465
|
description: "Using AttrRefs for cross-resource references",
|
|
460
466
|
mimeType: "text/typescript",
|
|
461
467
|
async handler(): Promise<string> {
|
|
462
|
-
return `import
|
|
468
|
+
return `import { Bucket, VersioningConfiguration, Role } from "@intentius/chant-lexicon-aws";
|
|
463
469
|
|
|
464
470
|
// Create a bucket
|
|
465
|
-
export const dataBucket = new
|
|
466
|
-
|
|
467
|
-
|
|
471
|
+
export const dataBucket = new Bucket({
|
|
472
|
+
BucketName: "my-data-bucket",
|
|
473
|
+
VersioningConfiguration: new VersioningConfiguration({ Status: "Enabled" }),
|
|
468
474
|
});
|
|
469
475
|
|
|
470
476
|
// Create a role that references the bucket's ARN
|
|
471
|
-
export const role = new
|
|
472
|
-
|
|
477
|
+
export const role = new Role({
|
|
478
|
+
AssumeRolePolicyDocument: {
|
|
473
479
|
Version: "2012-10-17",
|
|
474
480
|
Statement: [{
|
|
475
481
|
Effect: "Allow",
|
package/src/serializer.test.ts
CHANGED
|
@@ -18,9 +18,9 @@ class MockBucket implements Declarable {
|
|
|
18
18
|
readonly lexicon = "aws";
|
|
19
19
|
readonly entityType = "AWS::S3::Bucket";
|
|
20
20
|
readonly arn: AttrRef;
|
|
21
|
-
readonly props: {
|
|
21
|
+
readonly props: { BucketName?: string; VersioningConfiguration?: { Status: string } };
|
|
22
22
|
|
|
23
|
-
constructor(props: {
|
|
23
|
+
constructor(props: { BucketName?: string; VersioningConfiguration?: { Status: string } } = {}) {
|
|
24
24
|
this.props = props;
|
|
25
25
|
this.arn = new AttrRef(this, "Arn");
|
|
26
26
|
}
|
|
@@ -57,7 +57,7 @@ describe("awsSerializer.serialize", () => {
|
|
|
57
57
|
|
|
58
58
|
test("serializes resources", () => {
|
|
59
59
|
const entities = new Map<string, Declarable>();
|
|
60
|
-
entities.set("MyBucket", new MockBucket({
|
|
60
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "my-bucket" }));
|
|
61
61
|
|
|
62
62
|
const output = awsSerializer.serialize(entities);
|
|
63
63
|
const template = JSON.parse(output);
|
|
@@ -86,8 +86,8 @@ describe("awsSerializer.serialize", () => {
|
|
|
86
86
|
test("serializes nested properties", () => {
|
|
87
87
|
const entities = new Map<string, Declarable>();
|
|
88
88
|
entities.set("MyBucket", new MockBucket({
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
BucketName: "my-bucket",
|
|
90
|
+
VersioningConfiguration: { Status: "Enabled" },
|
|
91
91
|
}));
|
|
92
92
|
|
|
93
93
|
const output = awsSerializer.serialize(entities);
|
|
@@ -98,21 +98,20 @@ describe("awsSerializer.serialize", () => {
|
|
|
98
98
|
});
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
test("
|
|
101
|
+
test("passes through property names verbatim", () => {
|
|
102
102
|
const entities = new Map<string, Declarable>();
|
|
103
|
-
entities.set("MyBucket", new MockBucket({
|
|
103
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "test" }));
|
|
104
104
|
|
|
105
105
|
const output = awsSerializer.serialize(entities);
|
|
106
106
|
const template = JSON.parse(output);
|
|
107
107
|
|
|
108
108
|
expect(template.Resources.MyBucket.Properties.BucketName).toBeDefined();
|
|
109
|
-
expect(template.Resources.MyBucket.Properties.bucketName).toBeUndefined();
|
|
110
109
|
});
|
|
111
110
|
|
|
112
111
|
test("handles multiple resources", () => {
|
|
113
112
|
const entities = new Map<string, Declarable>();
|
|
114
|
-
entities.set("DataBucket", new MockBucket({
|
|
115
|
-
entities.set("LogsBucket", new MockBucket({
|
|
113
|
+
entities.set("DataBucket", new MockBucket({ BucketName: "data-bucket" }));
|
|
114
|
+
entities.set("LogsBucket", new MockBucket({ BucketName: "logs-bucket" }));
|
|
116
115
|
|
|
117
116
|
const output = awsSerializer.serialize(entities);
|
|
118
117
|
const template = JSON.parse(output);
|
|
@@ -125,7 +124,7 @@ describe("awsSerializer.serialize", () => {
|
|
|
125
124
|
test("handles resources and parameters together", () => {
|
|
126
125
|
const entities = new Map<string, Declarable>();
|
|
127
126
|
entities.set("Env", new Parameter("String"));
|
|
128
|
-
entities.set("MyBucket", new MockBucket({
|
|
127
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
129
128
|
|
|
130
129
|
const output = awsSerializer.serialize(entities);
|
|
131
130
|
const template = JSON.parse(output);
|
|
@@ -154,9 +153,9 @@ class MockEncryption implements Declarable {
|
|
|
154
153
|
readonly lexicon = "aws";
|
|
155
154
|
readonly entityType = "AWS::S3::Bucket.BucketEncryption";
|
|
156
155
|
readonly kind = "property" as const;
|
|
157
|
-
readonly props: {
|
|
156
|
+
readonly props: { ServerSideEncryptionConfiguration: unknown[] };
|
|
158
157
|
|
|
159
|
-
constructor(props: {
|
|
158
|
+
constructor(props: { ServerSideEncryptionConfiguration: unknown[] }) {
|
|
160
159
|
this.props = props;
|
|
161
160
|
}
|
|
162
161
|
}
|
|
@@ -164,14 +163,14 @@ class MockEncryption implements Declarable {
|
|
|
164
163
|
describe("property-kind Declarables", () => {
|
|
165
164
|
test("property-kind Declarables are inlined into parent properties", () => {
|
|
166
165
|
const encryption = new MockEncryption({
|
|
167
|
-
|
|
168
|
-
{
|
|
166
|
+
ServerSideEncryptionConfiguration: [
|
|
167
|
+
{ ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } },
|
|
169
168
|
],
|
|
170
169
|
});
|
|
171
170
|
|
|
172
|
-
const bucket = new MockBucket({
|
|
171
|
+
const bucket = new MockBucket({ BucketName: "my-bucket" });
|
|
173
172
|
// Manually set encryption as a prop
|
|
174
|
-
(bucket.props as Record<string, unknown>).
|
|
173
|
+
(bucket.props as Record<string, unknown>).BucketEncryption = encryption;
|
|
175
174
|
|
|
176
175
|
const entities = new Map<string, Declarable>();
|
|
177
176
|
entities.set("DataEncryption", encryption);
|
|
@@ -181,23 +180,23 @@ describe("property-kind Declarables", () => {
|
|
|
181
180
|
const template = JSON.parse(output);
|
|
182
181
|
|
|
183
182
|
// Encryption should be inlined, not a Ref
|
|
184
|
-
expect(template.Resources.MyBucket.Properties.
|
|
183
|
+
expect(template.Resources.MyBucket.Properties.BucketEncryption).toEqual({
|
|
185
184
|
ServerSideEncryptionConfiguration: [
|
|
186
|
-
{ ServerSideEncryptionByDefault: {
|
|
185
|
+
{ ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } },
|
|
187
186
|
],
|
|
188
187
|
});
|
|
189
188
|
});
|
|
190
189
|
|
|
191
190
|
test("property-kind Declarables do NOT appear as standalone Resources", () => {
|
|
192
191
|
const encryption = new MockEncryption({
|
|
193
|
-
|
|
194
|
-
{
|
|
192
|
+
ServerSideEncryptionConfiguration: [
|
|
193
|
+
{ ServerSideEncryptionByDefault: { SSEAlgorithm: "AES256" } },
|
|
195
194
|
],
|
|
196
195
|
});
|
|
197
196
|
|
|
198
197
|
const entities = new Map<string, Declarable>();
|
|
199
198
|
entities.set("DataEncryption", encryption);
|
|
200
|
-
entities.set("MyBucket", new MockBucket({
|
|
199
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "my-bucket" }));
|
|
201
200
|
|
|
202
201
|
const output = awsSerializer.serialize(entities);
|
|
203
202
|
const template = JSON.parse(output);
|
|
@@ -207,16 +206,16 @@ describe("property-kind Declarables", () => {
|
|
|
207
206
|
});
|
|
208
207
|
|
|
209
208
|
test("resource-kind Declarables still emit Ref when referenced", () => {
|
|
210
|
-
const sourceBucket = new MockBucket({
|
|
209
|
+
const sourceBucket = new MockBucket({ BucketName: "source" });
|
|
211
210
|
|
|
212
211
|
class MockConfig implements Declarable {
|
|
213
212
|
readonly [DECLARABLE_MARKER] = true as const;
|
|
214
213
|
readonly lexicon = "aws";
|
|
215
214
|
readonly entityType = "AWS::S3::ReplicationDestination";
|
|
216
|
-
readonly props: {
|
|
215
|
+
readonly props: { Bucket: Declarable };
|
|
217
216
|
|
|
218
|
-
constructor(
|
|
219
|
-
this.props = {
|
|
217
|
+
constructor(Bucket: Declarable) {
|
|
218
|
+
this.props = { Bucket };
|
|
220
219
|
}
|
|
221
220
|
}
|
|
222
221
|
|
|
@@ -233,7 +232,7 @@ describe("property-kind Declarables", () => {
|
|
|
233
232
|
|
|
234
233
|
describe("intrinsic serialization", () => {
|
|
235
234
|
test("handles AttrRef in properties", () => {
|
|
236
|
-
const source = new MockBucket({
|
|
235
|
+
const source = new MockBucket({ BucketName: "source" });
|
|
237
236
|
// Set the logical name on the AttrRef before using it
|
|
238
237
|
(source.arn as Record<string, unknown>)._setLogicalName("SourceBucket");
|
|
239
238
|
|
|
@@ -241,10 +240,10 @@ describe("intrinsic serialization", () => {
|
|
|
241
240
|
readonly [DECLARABLE_MARKER] = true as const;
|
|
242
241
|
readonly lexicon = "aws";
|
|
243
242
|
readonly entityType = "AWS::S3::ReplicationConfiguration";
|
|
244
|
-
readonly props: {
|
|
243
|
+
readonly props: { SourceArn: AttrRef };
|
|
245
244
|
|
|
246
|
-
constructor(
|
|
247
|
-
this.props = {
|
|
245
|
+
constructor(SourceArn: AttrRef) {
|
|
246
|
+
this.props = { SourceArn };
|
|
248
247
|
}
|
|
249
248
|
}
|
|
250
249
|
|
|
@@ -256,14 +255,14 @@ describe("intrinsic serialization", () => {
|
|
|
256
255
|
const template = JSON.parse(output);
|
|
257
256
|
|
|
258
257
|
expect(template.Resources.Replication.Properties.SourceArn).toEqual({
|
|
259
|
-
"Fn::
|
|
258
|
+
"Fn::GetAtt": ["SourceBucket", "Arn"],
|
|
260
259
|
});
|
|
261
260
|
});
|
|
262
261
|
});
|
|
263
262
|
|
|
264
263
|
describe("LexiconOutput serialization", () => {
|
|
265
264
|
test("generates CF Outputs section for LexiconOutputs", () => {
|
|
266
|
-
const bucket = new MockBucket({
|
|
265
|
+
const bucket = new MockBucket({ BucketName: "data-bucket" });
|
|
267
266
|
const lexiconOutput = new LexiconOutput(bucket.arn, "DataBucketArn");
|
|
268
267
|
lexiconOutput._setSourceEntity("dataBucket");
|
|
269
268
|
|
|
@@ -275,13 +274,13 @@ describe("LexiconOutput serialization", () => {
|
|
|
275
274
|
|
|
276
275
|
expect(template.Outputs).toBeDefined();
|
|
277
276
|
expect(template.Outputs.DataBucketArn).toEqual({
|
|
278
|
-
Value: { "Fn::
|
|
277
|
+
Value: { "Fn::GetAtt": ["dataBucket", "Arn"] },
|
|
279
278
|
});
|
|
280
279
|
});
|
|
281
280
|
|
|
282
281
|
test("generates multiple CF Outputs", () => {
|
|
283
|
-
const dataBucket = new MockBucket({
|
|
284
|
-
const logsBucket = new MockBucket({
|
|
282
|
+
const dataBucket = new MockBucket({ BucketName: "data-bucket" });
|
|
283
|
+
const logsBucket = new MockBucket({ BucketName: "logs-bucket" });
|
|
285
284
|
|
|
286
285
|
const dataOutput = new LexiconOutput(dataBucket.arn, "DataBucketArn");
|
|
287
286
|
dataOutput._setSourceEntity("dataBucket");
|
|
@@ -298,16 +297,16 @@ describe("LexiconOutput serialization", () => {
|
|
|
298
297
|
expect(template.Outputs).toBeDefined();
|
|
299
298
|
expect(Object.keys(template.Outputs)).toHaveLength(2);
|
|
300
299
|
expect(template.Outputs.DataBucketArn.Value).toEqual({
|
|
301
|
-
"Fn::
|
|
300
|
+
"Fn::GetAtt": ["dataBucket", "Arn"],
|
|
302
301
|
});
|
|
303
302
|
expect(template.Outputs.LogsBucketArn.Value).toEqual({
|
|
304
|
-
"Fn::
|
|
303
|
+
"Fn::GetAtt": ["logsBucket", "Arn"],
|
|
305
304
|
});
|
|
306
305
|
});
|
|
307
306
|
|
|
308
307
|
test("omits Outputs section when no LexiconOutputs provided", () => {
|
|
309
308
|
const entities = new Map<string, Declarable>();
|
|
310
|
-
entities.set("MyBucket", new MockBucket({
|
|
309
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
311
310
|
|
|
312
311
|
const result = awsSerializer.serialize(entities);
|
|
313
312
|
const template = JSON.parse(result);
|
|
@@ -317,7 +316,7 @@ describe("LexiconOutput serialization", () => {
|
|
|
317
316
|
|
|
318
317
|
test("omits Outputs section when empty LexiconOutputs array", () => {
|
|
319
318
|
const entities = new Map<string, Declarable>();
|
|
320
|
-
entities.set("MyBucket", new MockBucket({
|
|
319
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
321
320
|
|
|
322
321
|
const result = awsSerializer.serialize(entities, []);
|
|
323
322
|
const template = JSON.parse(result as string);
|
|
@@ -430,7 +429,7 @@ describe("nested stack serialization", () => {
|
|
|
430
429
|
Resources: {},
|
|
431
430
|
});
|
|
432
431
|
|
|
433
|
-
const bucket = new MockBucket({
|
|
432
|
+
const bucket = new MockBucket({ BucketName: "data" });
|
|
434
433
|
|
|
435
434
|
const entities = new Map<string, Declarable>();
|
|
436
435
|
entities.set("network", stack as unknown as Declarable);
|
|
@@ -446,7 +445,7 @@ describe("nested stack serialization", () => {
|
|
|
446
445
|
|
|
447
446
|
test("without nested stacks returns plain string", () => {
|
|
448
447
|
const entities = new Map<string, Declarable>();
|
|
449
|
-
entities.set("MyBucket", new MockBucket({
|
|
448
|
+
entities.set("MyBucket", new MockBucket({ BucketName: "bucket" }));
|
|
450
449
|
|
|
451
450
|
const result = awsSerializer.serialize(entities);
|
|
452
451
|
expect(typeof result).toBe("string");
|
|
@@ -460,7 +459,7 @@ describe("nested stack serialization", () => {
|
|
|
460
459
|
subnet: { Type: "AWS::EC2::Subnet" },
|
|
461
460
|
},
|
|
462
461
|
Outputs: {
|
|
463
|
-
subnetId: { Value: { "Fn::
|
|
462
|
+
subnetId: { Value: { "Fn::GetAtt": ["subnet", "SubnetId"] } },
|
|
464
463
|
},
|
|
465
464
|
});
|
|
466
465
|
|
|
@@ -472,7 +471,7 @@ describe("nested stack serialization", () => {
|
|
|
472
471
|
entityType: "AWS::Lambda::Function",
|
|
473
472
|
kind: "resource" as const,
|
|
474
473
|
props: {
|
|
475
|
-
|
|
474
|
+
VpcConfig: { SubnetIds: [subnetRef] },
|
|
476
475
|
},
|
|
477
476
|
} as unknown as Declarable;
|
|
478
477
|
|
package/src/serializer.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { isPropertyDeclarable } from "@intentius/chant/declarable";
|
|
|
3
3
|
import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
5
5
|
import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
|
|
6
|
-
import { toPascalCase } from "@intentius/chant/codegen/case";
|
|
7
6
|
import { isChildProject, type ChildProjectInstance } from "@intentius/chant/child-project";
|
|
8
7
|
import { isStackOutput, type StackOutput } from "@intentius/chant/stack-output";
|
|
9
8
|
|
|
@@ -68,7 +67,7 @@ interface CFOutput {
|
|
|
68
67
|
*/
|
|
69
68
|
function cfnVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
|
|
70
69
|
return {
|
|
71
|
-
attrRef: (name, attr) => ({ "Fn::
|
|
70
|
+
attrRef: (name, attr) => ({ "Fn::GetAtt": [name, attr] }),
|
|
72
71
|
resourceRef: (name) => ({ Ref: name }),
|
|
73
72
|
propertyDeclarable: (entity, walk) => {
|
|
74
73
|
if (!("props" in entity) || typeof entity.props !== "object" || entity.props === null) {
|
|
@@ -78,26 +77,19 @@ function cfnVisitor(entityNames: Map<Declarable, string>): SerializerVisitor {
|
|
|
78
77
|
const cfProps: Record<string, unknown> = {};
|
|
79
78
|
for (const [key, value] of Object.entries(props)) {
|
|
80
79
|
if (value !== undefined) {
|
|
81
|
-
|
|
82
|
-
cfProps[cfKey] = walk(value);
|
|
80
|
+
cfProps[key] = walk(value);
|
|
83
81
|
}
|
|
84
82
|
}
|
|
85
83
|
return Object.keys(cfProps).length > 0 ? cfProps : undefined;
|
|
86
84
|
},
|
|
87
|
-
transformKey: toPascalCase,
|
|
88
85
|
};
|
|
89
86
|
}
|
|
90
87
|
|
|
91
88
|
/**
|
|
92
89
|
* Convert a value to CF-compatible JSON using the generic walker.
|
|
93
90
|
*/
|
|
94
|
-
function toCFValue(value: unknown, entityNames: Map<Declarable, string
|
|
95
|
-
|
|
96
|
-
if (!convertKeys) {
|
|
97
|
-
// When not converting keys, use a visitor without transformKey
|
|
98
|
-
return walkValue(value, entityNames, { ...visitor, transformKey: undefined });
|
|
99
|
-
}
|
|
100
|
-
return walkValue(value, entityNames, visitor);
|
|
91
|
+
function toCFValue(value: unknown, entityNames: Map<Declarable, string>): unknown {
|
|
92
|
+
return walkValue(value, entityNames, cfnVisitor(entityNames));
|
|
101
93
|
}
|
|
102
94
|
|
|
103
95
|
/**
|
|
@@ -116,8 +108,7 @@ function toProperties(
|
|
|
116
108
|
|
|
117
109
|
for (const [key, value] of Object.entries(props)) {
|
|
118
110
|
if (value !== undefined) {
|
|
119
|
-
|
|
120
|
-
cfProps[cfKey] = toCFValue(value, entityNames, true);
|
|
111
|
+
cfProps[key] = toCFValue(value, entityNames);
|
|
121
112
|
}
|
|
122
113
|
}
|
|
123
114
|
|
|
@@ -231,7 +222,7 @@ function serializeToTemplate(
|
|
|
231
222
|
const logicalName = ref.getLogicalName();
|
|
232
223
|
if (logicalName) {
|
|
233
224
|
const output: CFOutput = {
|
|
234
|
-
Value: { "Fn::
|
|
225
|
+
Value: { "Fn::GetAtt": [logicalName, ref.attribute] },
|
|
235
226
|
};
|
|
236
227
|
if (stackOutput.description) {
|
|
237
228
|
output.Description = stackOutput.description;
|
|
@@ -247,7 +238,7 @@ function serializeToTemplate(
|
|
|
247
238
|
for (const output of outputs) {
|
|
248
239
|
template.Outputs[output.outputName] = {
|
|
249
240
|
Value: {
|
|
250
|
-
"Fn::
|
|
241
|
+
"Fn::GetAtt": [output.sourceEntity, output.sourceAttribute],
|
|
251
242
|
},
|
|
252
243
|
};
|
|
253
244
|
}
|