@intentius/chant-lexicon-aws 0.0.12 → 0.0.14
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 -24
- package/dist/manifest.json +1 -1
- package/dist/meta.json +795 -384
- package/dist/skills/chant-eks.md +175 -0
- package/dist/types/index.d.ts +449 -27
- package/package.json +2 -2
- package/src/codegen/docs.ts +93 -7
- package/src/codegen/generate-typescript.ts +1 -1
- package/src/codegen/generate.ts +3 -2
- package/src/composites/composites.test.ts +148 -0
- package/src/composites/index.ts +2 -0
- package/src/composites/rds-instance.ts +173 -0
- package/src/coverage.test.ts +31 -0
- package/src/generated/index.d.ts +449 -27
- package/src/generated/index.ts +88 -86
- package/src/generated/lexicon-aws.json +795 -384
- package/src/index.ts +2 -2
- package/src/plugin.ts +318 -1
- package/src/spec/parse.test.ts +50 -0
- package/src/spec/parse.ts +9 -6
- package/src/validate.test.ts +34 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-aws",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": ["src/", "dist/"],
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"prepack": "bun run bundle && bun run validate"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@intentius/chant": "0.0.
|
|
25
|
+
"@intentius/chant": "0.0.13",
|
|
26
26
|
"fflate": "^0.8.2",
|
|
27
27
|
"js-yaml": "^4.1.0"
|
|
28
28
|
},
|
package/src/codegen/docs.ts
CHANGED
|
@@ -149,22 +149,23 @@ When you reference a resource or attribute from another file (e.g. \`dataBucket.
|
|
|
149
149
|
|
|
150
150
|
CloudFormation parameters let you customize a stack at deploy time. Export a \`Parameter\` to add it to the template's \`Parameters\` section:
|
|
151
151
|
|
|
152
|
-
{{file:docs-snippets/src/parameter-
|
|
152
|
+
{{file:docs-snippets/src/parameter-declaration.ts}}
|
|
153
153
|
|
|
154
154
|
Produces:
|
|
155
155
|
|
|
156
156
|
\`\`\`json
|
|
157
157
|
"Parameters": {
|
|
158
|
-
"
|
|
158
|
+
"Environment": {
|
|
159
159
|
"Type": "String",
|
|
160
|
-
"
|
|
160
|
+
"Default": "dev",
|
|
161
|
+
"Description": "Deployment environment"
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
164
|
\`\`\`
|
|
164
165
|
|
|
165
166
|
Reference parameters with \`Ref\`:
|
|
166
167
|
|
|
167
|
-
{{file:docs-snippets/src/parameter-ref.ts}}
|
|
168
|
+
{{file:docs-snippets/src/parameter-cross-file-ref.ts}}
|
|
168
169
|
|
|
169
170
|
## Outputs
|
|
170
171
|
|
|
@@ -201,7 +202,7 @@ Runtime context values available in every template, accessed via the \`AWS\` nam
|
|
|
201
202
|
|
|
202
203
|
## Intrinsic functions
|
|
203
204
|
|
|
204
|
-
The lexicon provides
|
|
205
|
+
The lexicon provides 9 intrinsic functions (\`Sub\`, \`Ref\`, \`GetAtt\`, \`If\`, \`Join\`, \`Select\`, \`Split\`, \`Base64\`, \`GetAZs\`) that map directly to CloudFormation \`Fn::\` calls. See [Intrinsic Functions](../intrinsics/) for full usage examples.
|
|
205
206
|
|
|
206
207
|
## Dependencies
|
|
207
208
|
|
|
@@ -369,7 +370,13 @@ Splits a string by a delimiter:
|
|
|
369
370
|
|
|
370
371
|
Encodes a string to Base64, commonly used for EC2 user data:
|
|
371
372
|
|
|
372
|
-
{{file:docs-snippets/src/intrinsics-detail.ts:23-27}}
|
|
373
|
+
{{file:docs-snippets/src/intrinsics-detail.ts:23-27}}
|
|
374
|
+
|
|
375
|
+
## \`GetAZs\` — availability zones
|
|
376
|
+
|
|
377
|
+
Returns the list of Availability Zones for a region:
|
|
378
|
+
|
|
379
|
+
{{file:docs-snippets/src/intrinsics-detail.ts:29-31}}`,
|
|
373
380
|
},
|
|
374
381
|
{
|
|
375
382
|
slug: "composites",
|
|
@@ -407,6 +414,7 @@ The AWS lexicon ships ready-to-use composites for common patterns. Import them f
|
|
|
407
414
|
| \`FargateAlb\` | \`cluster\`, \`executionRole\`, \`taskRole\`, \`logGroup\`, \`taskDef\`, \`albSg\`, \`taskSg\`, \`alb\`, \`targetGroup\`, \`listener\`, \`service\` | Fargate service behind an ALB. Accepts VPC outputs as props. |
|
|
408
415
|
| \`AlbShared\` | \`cluster\`, \`executionRole\`, \`albSg\`, \`alb\`, \`listener\` | Shared ALB infrastructure (ECS cluster, execution role, ALB, listener with 404 default). Created once, consumed by multiple \`FargateService\` instances. |
|
|
409
416
|
| \`FargateService\` | \`taskRole\`, \`logGroup\`, \`taskDef\`, \`taskSg\`, \`targetGroup\`, \`rule\`, \`service\` | Per-service Fargate resources with listener rule routing. Wire to an \`AlbShared\` instance for multi-service ALB patterns. |
|
|
417
|
+
| \`RdsInstance\` | \`subnetGroup\`, \`sg\`, \`db\` (+ \`parameterGroup\` if configured) | RDS instance (postgres, mysql, mariadb) in private subnets. Creates DB subnet group, security group, and optionally a parameter group. Engine-specific defaults for port, username, and version. Encrypted by default. |
|
|
410
418
|
|
|
411
419
|
All built-in composites accept \`ManagedPolicyArns\` and \`Policies\` for adding IAM permissions to the auto-created role.
|
|
412
420
|
|
|
@@ -749,6 +757,72 @@ WAW030: API Gateway Deployment "MyDeployment" has no DependsOn on any Method
|
|
|
749
757
|
WAW030: ScalableTarget "MyTarget" targets DynamoDB but has no DependsOn on any Table
|
|
750
758
|
\`\`\`
|
|
751
759
|
|
|
760
|
+
### WAW018 — S3 Bucket Missing Public Access Block
|
|
761
|
+
|
|
762
|
+
**Severity:** error | **Category:** security
|
|
763
|
+
|
|
764
|
+
Flags S3 buckets without a \`PublicAccessBlockConfiguration\`. Without an explicit public access block, the bucket may be publicly accessible. Always set \`BlockPublicAcls\`, \`BlockPublicPolicy\`, \`IgnorePublicAcls\`, and \`RestrictPublicBuckets\` to \`true\`.
|
|
765
|
+
|
|
766
|
+
### WAW019 — Security Group Unrestricted Ingress on Sensitive Ports
|
|
767
|
+
|
|
768
|
+
**Severity:** error | **Category:** security
|
|
769
|
+
|
|
770
|
+
Flags security group ingress rules that allow unrestricted access (\`0.0.0.0/0\` or \`::/0\`) on sensitive ports (22, 3389, 3306, 5432, 1433, 6379, 27017). Restrict ingress to known CIDR ranges or security groups.
|
|
771
|
+
|
|
772
|
+
### WAW020 — IAM Policy Uses Wildcard Action
|
|
773
|
+
|
|
774
|
+
**Severity:** warning | **Category:** security
|
|
775
|
+
|
|
776
|
+
Flags IAM policy statements that use wildcard actions (\`"Action": "*"\` or \`"Action": "s3:*"\`). Use specific action names following the principle of least privilege.
|
|
777
|
+
|
|
778
|
+
### WAW021 — RDS Storage Not Encrypted
|
|
779
|
+
|
|
780
|
+
**Severity:** error | **Category:** security
|
|
781
|
+
|
|
782
|
+
Flags RDS instances without \`StorageEncrypted: true\`. All RDS instances should encrypt data at rest to meet compliance and security requirements.
|
|
783
|
+
|
|
784
|
+
### WAW022 — Lambda Not in VPC
|
|
785
|
+
|
|
786
|
+
**Severity:** warning | **Category:** security
|
|
787
|
+
|
|
788
|
+
Flags Lambda functions without a \`VpcConfig\`. Functions that access internal resources (databases, caches, internal APIs) should run inside a VPC. Functions that only call public APIs can safely skip VPC configuration.
|
|
789
|
+
|
|
790
|
+
### WAW023 — CloudFront Without WAF
|
|
791
|
+
|
|
792
|
+
**Severity:** warning | **Category:** security
|
|
793
|
+
|
|
794
|
+
Flags CloudFront distributions without a \`WebACLId\`. Attaching a WAF web ACL protects your distribution from common web exploits and bots.
|
|
795
|
+
|
|
796
|
+
### WAW024 — ALB Without Access Logging
|
|
797
|
+
|
|
798
|
+
**Severity:** warning | **Category:** best practice
|
|
799
|
+
|
|
800
|
+
Flags Application Load Balancers without access logging enabled. Enable \`access_logs.s3.enabled\` to capture request logs for debugging and compliance.
|
|
801
|
+
|
|
802
|
+
### WAW025 — SNS Topic Not Encrypted
|
|
803
|
+
|
|
804
|
+
**Severity:** warning | **Category:** security
|
|
805
|
+
|
|
806
|
+
Flags SNS topics without \`KmsMasterKeyId\`. Encrypting topics at rest protects sensitive notification payloads.
|
|
807
|
+
|
|
808
|
+
### WAW026 — SQS Queue Not Encrypted
|
|
809
|
+
|
|
810
|
+
**Severity:** warning | **Category:** security
|
|
811
|
+
|
|
812
|
+
Flags SQS queues without \`KmsMasterKeyId\` or \`SqsManagedSseEnabled\`. Encrypting queues at rest protects sensitive message payloads.
|
|
813
|
+
|
|
814
|
+
### WAW027 — DynamoDB Missing Point-in-Time Recovery
|
|
815
|
+
|
|
816
|
+
**Severity:** info | **Category:** best practice
|
|
817
|
+
|
|
818
|
+
Flags DynamoDB tables without \`PointInTimeRecoverySpecification.PointInTimeRecoveryEnabled\` set to \`true\`. Point-in-time recovery provides continuous backups and protects against accidental writes or deletes.
|
|
819
|
+
|
|
820
|
+
### WAW028 — EBS Volume Not Encrypted
|
|
821
|
+
|
|
822
|
+
**Severity:** warning | **Category:** security
|
|
823
|
+
|
|
824
|
+
Flags EBS volumes without \`Encrypted: true\`. All EBS volumes should encrypt data at rest for compliance and security.
|
|
825
|
+
|
|
752
826
|
## Running lint
|
|
753
827
|
|
|
754
828
|
\`\`\`bash
|
|
@@ -953,7 +1027,19 @@ src/
|
|
|
953
1027
|
- **Composite presets** — \`SecureApi\` (low memory, short timeout) and \`HighMemoryApi\` (high memory, longer timeout)
|
|
954
1028
|
- **Custom lint rule** — \`api-timeout.ts\` enforces API Gateway's 29-second timeout limit (see [Custom Lint Rules](../custom-rules/))
|
|
955
1029
|
|
|
956
|
-
The example produces 10 CloudFormation resources: 1 S3 bucket + 3 composites × 3 members each
|
|
1030
|
+
The example produces 10 CloudFormation resources: 1 S3 bucket + 3 composites × 3 members each.
|
|
1031
|
+
|
|
1032
|
+
## RDS Instance
|
|
1033
|
+
|
|
1034
|
+
\`examples/rds-postgres/\` — production RDS PostgreSQL instance using the \`RdsInstance\` composite with VPC networking and SSM parameter references.
|
|
1035
|
+
|
|
1036
|
+
{{file:rds-postgres/src/params.ts}}
|
|
1037
|
+
|
|
1038
|
+
{{file:rds-postgres/src/network.ts}}
|
|
1039
|
+
|
|
1040
|
+
{{file:rds-postgres/src/database.ts}}
|
|
1041
|
+
|
|
1042
|
+
Produces a complete RDS stack: VPC infrastructure (from \`VpcDefault\`), DB subnet group, security group, and RDS instance with encrypted storage.`,
|
|
957
1043
|
},
|
|
958
1044
|
{
|
|
959
1045
|
slug: "skills",
|
|
@@ -78,7 +78,7 @@ export function generateTypeScriptDeclarations(
|
|
|
78
78
|
description: p.description,
|
|
79
79
|
}));
|
|
80
80
|
const dtsAttrs: DtsAttribute[] = r.resource.attributes.map((a) => ({
|
|
81
|
-
name: a.name,
|
|
81
|
+
name: a.name.replace(/\./g, "_").replace(/\*/g, "Item"), // Subscribers.*.Status → Subscribers_Item_Status
|
|
82
82
|
type: a.tsType,
|
|
83
83
|
}));
|
|
84
84
|
|
package/src/codegen/generate.ts
CHANGED
|
@@ -170,10 +170,11 @@ function generateRuntimeIndex(
|
|
|
170
170
|
const tsName = naming.resolve(cfnType);
|
|
171
171
|
if (!tsName) continue;
|
|
172
172
|
|
|
173
|
-
// Build attrs map
|
|
173
|
+
// Build attrs map: TS key (underscores) → CF attr name (dots)
|
|
174
174
|
const attrs: Record<string, string> = {};
|
|
175
175
|
for (const a of r.resource.attributes) {
|
|
176
|
-
|
|
176
|
+
const tsKey = a.name.replace(/\./g, "_").replace(/\*/g, "Item"); // Subscribers.*.Status → Subscribers_Item_Status
|
|
177
|
+
attrs[tsKey] = a.name; // maps to "Endpoint.Address" for GetAtt
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
resourceEntries.push({ tsName, resourceType: cfnType, attrs });
|
|
@@ -13,6 +13,7 @@ import { VpcDefault } from "./vpc-default";
|
|
|
13
13
|
import { FargateAlb } from "./fargate-alb";
|
|
14
14
|
import { AlbShared } from "./alb-shared";
|
|
15
15
|
import { FargateService } from "./fargate-service";
|
|
16
|
+
import { RdsInstance } from "./rds-instance";
|
|
16
17
|
|
|
17
18
|
const baseProps = {
|
|
18
19
|
name: "TestFunc",
|
|
@@ -633,3 +634,150 @@ describe("FargateService", () => {
|
|
|
633
634
|
expect(svcProps.DesiredCount).toBe(2);
|
|
634
635
|
});
|
|
635
636
|
});
|
|
637
|
+
|
|
638
|
+
describe("RdsInstance", () => {
|
|
639
|
+
const rdsProps = {
|
|
640
|
+
vpcId: "vpc-123",
|
|
641
|
+
subnetIds: ["subnet-1", "subnet-2"],
|
|
642
|
+
masterPassword: "secret",
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
test("returns subnetGroup, sg, db members", () => {
|
|
646
|
+
const instance = RdsInstance(rdsProps);
|
|
647
|
+
const names = Object.keys(instance.members);
|
|
648
|
+
expect(names).toContain("subnetGroup");
|
|
649
|
+
expect(names).toContain("sg");
|
|
650
|
+
expect(names).toContain("db");
|
|
651
|
+
expect(names).toHaveLength(3);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test("expandComposite produces correct logical names", () => {
|
|
655
|
+
const expanded = expandComposite("myDb", RdsInstance(rdsProps));
|
|
656
|
+
expect(expanded.has("myDbSubnetGroup")).toBe(true);
|
|
657
|
+
expect(expanded.has("myDbSg")).toBe(true);
|
|
658
|
+
expect(expanded.has("myDbDb")).toBe(true);
|
|
659
|
+
expect(expanded.size).toBe(3);
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test("with parameterGroupFamily, also returns parameterGroup", () => {
|
|
663
|
+
const instance = RdsInstance({
|
|
664
|
+
...rdsProps,
|
|
665
|
+
parameterGroupFamily: "postgres16",
|
|
666
|
+
parameters: { shared_preload_libraries: "pg_stat_statements" },
|
|
667
|
+
});
|
|
668
|
+
const names = Object.keys(instance.members);
|
|
669
|
+
expect(names).toContain("parameterGroup");
|
|
670
|
+
expect(names).toHaveLength(4);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
test("expandComposite with parameterGroup produces 4 entries", () => {
|
|
674
|
+
const expanded = expandComposite("pg", RdsInstance({
|
|
675
|
+
...rdsProps,
|
|
676
|
+
parameterGroupFamily: "postgres16",
|
|
677
|
+
}));
|
|
678
|
+
expect(expanded.has("pgSubnetGroup")).toBe(true);
|
|
679
|
+
expect(expanded.has("pgSg")).toBe(true);
|
|
680
|
+
expect(expanded.has("pgDb")).toBe(true);
|
|
681
|
+
expect(expanded.has("pgParameterGroup")).toBe(true);
|
|
682
|
+
expect(expanded.size).toBe(4);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test("ingress from SG produces SourceSecurityGroupId rule", () => {
|
|
686
|
+
const instance = RdsInstance({
|
|
687
|
+
...rdsProps,
|
|
688
|
+
ingressSourceSG: "sg-app123",
|
|
689
|
+
});
|
|
690
|
+
const sgProps = (instance.sg as any).props;
|
|
691
|
+
expect(sgProps.SecurityGroupIngress).toHaveLength(1);
|
|
692
|
+
const ingress = (sgProps.SecurityGroupIngress[0] as any).props;
|
|
693
|
+
expect(ingress.SourceSecurityGroupId).toBe("sg-app123");
|
|
694
|
+
expect(ingress.FromPort).toBe(5432);
|
|
695
|
+
expect(ingress.ToPort).toBe(5432);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
test("ingress from CIDR produces CidrIp rule", () => {
|
|
699
|
+
const instance = RdsInstance({
|
|
700
|
+
...rdsProps,
|
|
701
|
+
ingressCidr: "10.0.0.0/16",
|
|
702
|
+
});
|
|
703
|
+
const sgProps = (instance.sg as any).props;
|
|
704
|
+
expect(sgProps.SecurityGroupIngress).toHaveLength(1);
|
|
705
|
+
const ingress = (sgProps.SecurityGroupIngress[0] as any).props;
|
|
706
|
+
expect(ingress.CidrIp).toBe("10.0.0.0/16");
|
|
707
|
+
expect(ingress.FromPort).toBe(5432);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
test("no ingress when neither SG nor CIDR provided", () => {
|
|
711
|
+
const instance = RdsInstance(rdsProps);
|
|
712
|
+
const sgProps = (instance.sg as any).props;
|
|
713
|
+
expect(sgProps.SecurityGroupIngress).toBeUndefined();
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
test("default engine is postgres with correct defaults", () => {
|
|
717
|
+
const instance = RdsInstance(rdsProps);
|
|
718
|
+
const dbProps = (instance.db as any).props;
|
|
719
|
+
expect(dbProps.Engine).toBe("postgres");
|
|
720
|
+
expect(dbProps.EngineVersion).toBe("16.6");
|
|
721
|
+
expect(dbProps.DBInstanceClass).toBe("db.t4g.micro");
|
|
722
|
+
expect(dbProps.AllocatedStorage).toBe("20");
|
|
723
|
+
expect(dbProps.StorageType).toBe("gp3");
|
|
724
|
+
expect(dbProps.StorageEncrypted).toBe(true);
|
|
725
|
+
expect(dbProps.MultiAZ).toBe(false);
|
|
726
|
+
expect(dbProps.BackupRetentionPeriod).toBe(7);
|
|
727
|
+
expect(dbProps.CopyTagsToSnapshot).toBe(true);
|
|
728
|
+
expect(dbProps.AutoMinorVersionUpgrade).toBe(true);
|
|
729
|
+
expect(dbProps.PubliclyAccessible).toBe(false);
|
|
730
|
+
expect(dbProps.DeletionProtection).toBe(false);
|
|
731
|
+
expect(dbProps.MasterUsername).toBe("postgres");
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
test("engine: mysql uses mysql-specific defaults", () => {
|
|
735
|
+
const instance = RdsInstance({
|
|
736
|
+
...rdsProps,
|
|
737
|
+
engine: "mysql",
|
|
738
|
+
ingressCidr: "10.0.0.0/16",
|
|
739
|
+
});
|
|
740
|
+
const dbProps = (instance.db as any).props;
|
|
741
|
+
expect(dbProps.Engine).toBe("mysql");
|
|
742
|
+
expect(dbProps.EngineVersion).toBe("8.0.40");
|
|
743
|
+
expect(dbProps.MasterUsername).toBe("admin");
|
|
744
|
+
expect(dbProps.Port).toBe("3306");
|
|
745
|
+
const ingress = ((instance.sg as any).props.SecurityGroupIngress[0] as any).props;
|
|
746
|
+
expect(ingress.FromPort).toBe(3306);
|
|
747
|
+
expect(ingress.ToPort).toBe(3306);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
test("engine: mariadb uses mariadb-specific defaults", () => {
|
|
751
|
+
const instance = RdsInstance({
|
|
752
|
+
...rdsProps,
|
|
753
|
+
engine: "mariadb",
|
|
754
|
+
});
|
|
755
|
+
const dbProps = (instance.db as any).props;
|
|
756
|
+
expect(dbProps.Engine).toBe("mariadb");
|
|
757
|
+
expect(dbProps.EngineVersion).toBe("11.4.3");
|
|
758
|
+
expect(dbProps.MasterUsername).toBe("admin");
|
|
759
|
+
expect(dbProps.Port).toBe("3306");
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test("custom port is applied to SG and DB", () => {
|
|
763
|
+
const instance = RdsInstance({
|
|
764
|
+
...rdsProps,
|
|
765
|
+
port: 3306,
|
|
766
|
+
ingressCidr: "10.0.0.0/8",
|
|
767
|
+
});
|
|
768
|
+
const dbProps = (instance.db as any).props;
|
|
769
|
+
expect(dbProps.Port).toBe("3306");
|
|
770
|
+
const ingress = ((instance.sg as any).props.SecurityGroupIngress[0] as any).props;
|
|
771
|
+
expect(ingress.FromPort).toBe(3306);
|
|
772
|
+
expect(ingress.ToPort).toBe(3306);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
test("db references subnet group and security group", () => {
|
|
776
|
+
const instance = RdsInstance(rdsProps);
|
|
777
|
+
const dbProps = (instance.db as any).props;
|
|
778
|
+
// Subnet group is passed as a resource instance (serializer resolves to { Ref: ... })
|
|
779
|
+
expect(dbProps.DBSubnetGroupName).toBe(instance.subnetGroup);
|
|
780
|
+
expect(dbProps.VPCSecurityGroups).toHaveLength(1);
|
|
781
|
+
expect(dbProps.VPCSecurityGroups[0]).toBeInstanceOf(AttrRef);
|
|
782
|
+
});
|
|
783
|
+
});
|
package/src/composites/index.ts
CHANGED
|
@@ -22,3 +22,5 @@ export { AlbShared } from "./alb-shared";
|
|
|
22
22
|
export type { AlbSharedProps } from "./alb-shared";
|
|
23
23
|
export { FargateService } from "./fargate-service";
|
|
24
24
|
export type { FargateServiceProps } from "./fargate-service";
|
|
25
|
+
export { RdsInstance, RdsInstance as RdsPostgres } from "./rds-instance";
|
|
26
|
+
export type { RdsInstanceProps, RdsInstanceProps as RdsPostgresProps } from "./rds-instance";
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Composite } from "@intentius/chant";
|
|
2
|
+
import {
|
|
3
|
+
DbInstance,
|
|
4
|
+
RDSDBSubnetGroup,
|
|
5
|
+
RDSDBParameterGroup,
|
|
6
|
+
SecurityGroup,
|
|
7
|
+
SecurityGroup_Ingress,
|
|
8
|
+
} from "../generated";
|
|
9
|
+
|
|
10
|
+
const ENGINE_DEFAULTS: Record<string, { port: number; username: string; version: string; logExport: string }> = {
|
|
11
|
+
postgres: { port: 5432, username: "postgres", version: "16.6", logExport: "postgresql" },
|
|
12
|
+
mysql: { port: 3306, username: "admin", version: "8.0.40", logExport: "general" },
|
|
13
|
+
mariadb: { port: 3306, username: "admin", version: "11.4.3", logExport: "general" },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface RdsInstanceProps {
|
|
17
|
+
// ── Engine ──────────────────────────────────────────────────────
|
|
18
|
+
engine?: "postgres" | "mysql" | "mariadb";
|
|
19
|
+
|
|
20
|
+
// ── Networking (required) ─────────────────────────────────────
|
|
21
|
+
vpcId: string;
|
|
22
|
+
subnetIds: string[];
|
|
23
|
+
ingressSourceSG?: string;
|
|
24
|
+
ingressCidr?: string;
|
|
25
|
+
port?: number;
|
|
26
|
+
publiclyAccessible?: boolean;
|
|
27
|
+
|
|
28
|
+
// ── Identity & auth (required) ────────────────────────────────
|
|
29
|
+
masterUsername?: string;
|
|
30
|
+
masterPassword: string;
|
|
31
|
+
|
|
32
|
+
// ── Engine version ──────────────────────────────────────────────
|
|
33
|
+
engineVersion?: string;
|
|
34
|
+
databaseName?: string;
|
|
35
|
+
|
|
36
|
+
// ── Instance sizing ───────────────────────────────────────────
|
|
37
|
+
instanceClass?: string;
|
|
38
|
+
allocatedStorage?: number;
|
|
39
|
+
storageType?: string;
|
|
40
|
+
maxAllocatedStorage?: number;
|
|
41
|
+
|
|
42
|
+
// ── High availability ─────────────────────────────────────────
|
|
43
|
+
multiAZ?: boolean;
|
|
44
|
+
|
|
45
|
+
// ── Encryption ────────────────────────────────────────────────
|
|
46
|
+
storageEncrypted?: boolean;
|
|
47
|
+
kmsKeyId?: string;
|
|
48
|
+
|
|
49
|
+
// ── Backup ────────────────────────────────────────────────────
|
|
50
|
+
backupRetentionPeriod?: number;
|
|
51
|
+
preferredBackupWindow?: string;
|
|
52
|
+
copyTagsToSnapshot?: boolean;
|
|
53
|
+
|
|
54
|
+
// ── Maintenance ───────────────────────────────────────────────
|
|
55
|
+
preferredMaintenanceWindow?: string;
|
|
56
|
+
autoMinorVersionUpgrade?: boolean;
|
|
57
|
+
|
|
58
|
+
// ── Monitoring ────────────────────────────────────────────────
|
|
59
|
+
enableCloudwatchLogs?: boolean;
|
|
60
|
+
enablePerformanceInsights?: boolean;
|
|
61
|
+
performanceInsightsRetentionPeriod?: number;
|
|
62
|
+
|
|
63
|
+
// ── Parameter group ───────────────────────────────────────────
|
|
64
|
+
parameterGroupFamily?: string;
|
|
65
|
+
parameters?: Record<string, string>;
|
|
66
|
+
|
|
67
|
+
// ── Protection ────────────────────────────────────────────────
|
|
68
|
+
deletionProtection?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const RdsInstance = Composite<RdsInstanceProps>((props) => {
|
|
72
|
+
const engine = props.engine ?? "postgres";
|
|
73
|
+
const defaults = ENGINE_DEFAULTS[engine];
|
|
74
|
+
const port = props.port ?? defaults.port;
|
|
75
|
+
const masterUsername = props.masterUsername ?? defaults.username;
|
|
76
|
+
const engineVersion = props.engineVersion ?? defaults.version;
|
|
77
|
+
const instanceClass = props.instanceClass ?? "db.t4g.micro";
|
|
78
|
+
const allocatedStorage = props.allocatedStorage ?? 20;
|
|
79
|
+
const storageType = props.storageType ?? "gp3";
|
|
80
|
+
const multiAZ = props.multiAZ ?? false;
|
|
81
|
+
const storageEncrypted = props.storageEncrypted ?? true;
|
|
82
|
+
const backupRetentionPeriod = props.backupRetentionPeriod ?? 7;
|
|
83
|
+
const copyTagsToSnapshot = props.copyTagsToSnapshot ?? true;
|
|
84
|
+
const autoMinorVersionUpgrade = props.autoMinorVersionUpgrade ?? true;
|
|
85
|
+
const publiclyAccessible = props.publiclyAccessible ?? false;
|
|
86
|
+
const deletionProtection = props.deletionProtection ?? false;
|
|
87
|
+
|
|
88
|
+
// DB Subnet Group
|
|
89
|
+
const subnetGroup = new RDSDBSubnetGroup({
|
|
90
|
+
DBSubnetGroupDescription: "Subnet group for RDS instance",
|
|
91
|
+
SubnetIds: props.subnetIds,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Security Group
|
|
95
|
+
const ingressRules: InstanceType<typeof SecurityGroup_Ingress>[] = [];
|
|
96
|
+
if (props.ingressSourceSG) {
|
|
97
|
+
ingressRules.push(
|
|
98
|
+
new SecurityGroup_Ingress({
|
|
99
|
+
IpProtocol: "tcp",
|
|
100
|
+
FromPort: port,
|
|
101
|
+
ToPort: port,
|
|
102
|
+
SourceSecurityGroupId: props.ingressSourceSG,
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
} else if (props.ingressCidr) {
|
|
106
|
+
ingressRules.push(
|
|
107
|
+
new SecurityGroup_Ingress({
|
|
108
|
+
IpProtocol: "tcp",
|
|
109
|
+
FromPort: port,
|
|
110
|
+
ToPort: port,
|
|
111
|
+
CidrIp: props.ingressCidr,
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const sg = new SecurityGroup({
|
|
117
|
+
GroupDescription: "Security group for RDS instance",
|
|
118
|
+
VpcId: props.vpcId,
|
|
119
|
+
SecurityGroupIngress: ingressRules.length > 0 ? ingressRules : undefined,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Optional Parameter Group
|
|
123
|
+
let parameterGroup: InstanceType<typeof RDSDBParameterGroup> | undefined;
|
|
124
|
+
if (props.parameterGroupFamily) {
|
|
125
|
+
parameterGroup = new RDSDBParameterGroup({
|
|
126
|
+
Family: props.parameterGroupFamily,
|
|
127
|
+
Description: "Custom parameter group",
|
|
128
|
+
Parameters: props.parameters,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// DB Instance
|
|
133
|
+
const dbProps: Record<string, any> = {
|
|
134
|
+
Engine: engine,
|
|
135
|
+
EngineVersion: engineVersion,
|
|
136
|
+
DBInstanceClass: instanceClass,
|
|
137
|
+
MasterUsername: masterUsername,
|
|
138
|
+
MasterUserPassword: props.masterPassword,
|
|
139
|
+
AllocatedStorage: String(allocatedStorage),
|
|
140
|
+
StorageType: storageType,
|
|
141
|
+
DBSubnetGroupName: subnetGroup.Ref,
|
|
142
|
+
VPCSecurityGroups: [sg.GroupId],
|
|
143
|
+
Port: String(port),
|
|
144
|
+
PubliclyAccessible: publiclyAccessible,
|
|
145
|
+
MultiAZ: multiAZ,
|
|
146
|
+
StorageEncrypted: storageEncrypted,
|
|
147
|
+
BackupRetentionPeriod: backupRetentionPeriod,
|
|
148
|
+
CopyTagsToSnapshot: copyTagsToSnapshot,
|
|
149
|
+
AutoMinorVersionUpgrade: autoMinorVersionUpgrade,
|
|
150
|
+
DeletionProtection: deletionProtection,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (props.databaseName) dbProps.DBName = props.databaseName;
|
|
154
|
+
if (props.kmsKeyId) dbProps.KmsKeyId = props.kmsKeyId;
|
|
155
|
+
if (props.maxAllocatedStorage) dbProps.MaxAllocatedStorage = props.maxAllocatedStorage;
|
|
156
|
+
if (props.preferredBackupWindow) dbProps.PreferredBackupWindow = props.preferredBackupWindow;
|
|
157
|
+
if (props.preferredMaintenanceWindow) dbProps.PreferredMaintenanceWindow = props.preferredMaintenanceWindow;
|
|
158
|
+
if (props.enableCloudwatchLogs) dbProps.EnableCloudwatchLogsExports = [defaults.logExport];
|
|
159
|
+
if (props.enablePerformanceInsights) {
|
|
160
|
+
dbProps.EnablePerformanceInsights = true;
|
|
161
|
+
if (props.performanceInsightsRetentionPeriod) {
|
|
162
|
+
dbProps.PerformanceInsightsRetentionPeriod = props.performanceInsightsRetentionPeriod;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (parameterGroup) dbProps.DBParameterGroupName = parameterGroup.Ref;
|
|
166
|
+
|
|
167
|
+
const db = new DbInstance(dbProps);
|
|
168
|
+
|
|
169
|
+
const result: Record<string, any> = { subnetGroup, sg, db };
|
|
170
|
+
if (parameterGroup) result.parameterGroup = parameterGroup;
|
|
171
|
+
|
|
172
|
+
return result;
|
|
173
|
+
}, "RdsInstance");
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
7
|
+
const generatedDir = join(pkgDir, "src", "generated");
|
|
8
|
+
const hasGenerated = existsSync(join(generatedDir, "lexicon-aws.json"));
|
|
9
|
+
|
|
10
|
+
describe("coverage", () => {
|
|
11
|
+
test.skipIf(!hasGenerated)("computeCoverage function exists", async () => {
|
|
12
|
+
const { computeCoverage } = await import("./coverage");
|
|
13
|
+
expect(typeof computeCoverage).toBe("function");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test.skipIf(!hasGenerated)("overallPct function exists", async () => {
|
|
17
|
+
const { overallPct } = await import("./coverage");
|
|
18
|
+
expect(typeof overallPct).toBe("function");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("handles missing generated files gracefully", async () => {
|
|
22
|
+
const { computeCoverage } = await import("./coverage");
|
|
23
|
+
if (!hasGenerated) {
|
|
24
|
+
try {
|
|
25
|
+
await computeCoverage(generatedDir);
|
|
26
|
+
} catch {
|
|
27
|
+
// Expected — no generated files
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|