@intentius/chant-lexicon-aws 0.0.14 → 0.0.16

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/src/plugin.ts CHANGED
@@ -285,10 +285,11 @@ export const logsBucket = new Bucket({
285
285
  const { waw028 } = require("./lint/post-synth/waw028");
286
286
  const { waw029 } = require("./lint/post-synth/waw029");
287
287
  const { waw030 } = require("./lint/post-synth/waw030");
288
+ const { waw031 } = require("./lint/post-synth/waw031");
288
289
  return [
289
290
  waw010, waw011, cor020, ext001, waw013, waw014, waw015, waw016, waw017,
290
291
  waw018, waw019, waw020, waw021, waw022, waw023, waw024, waw025,
291
- waw026, waw027, waw028, waw029, waw030,
292
+ waw026, waw027, waw028, waw029, waw030, waw031,
292
293
  ];
293
294
  },
294
295
 
@@ -898,219 +899,50 @@ aws cloudformation wait stack-update-complete --stack-name my-app-prod`,
898
899
  },
899
900
  ],
900
901
  },
902
+ ];
903
+
904
+ // Load file-based skills from src/skills/
905
+ const { readFileSync } = require("fs");
906
+ const { join, dirname } = require("path");
907
+ const { fileURLToPath } = require("url");
908
+ const dir = dirname(fileURLToPath(import.meta.url));
909
+
910
+ const skillFiles = [
901
911
  {
912
+ file: "chant-eks.md",
902
913
  name: "chant-eks",
903
914
  description: "EKS end-to-end workflow — provision cluster, configure kubectl, deploy K8s workloads",
904
- content: `---
905
- skill: chant-eks
906
- description: End-to-end EKS workflow bridging AWS infrastructure and Kubernetes workloads
907
- user-invocable: true
908
- ---
909
-
910
- # EKS End-to-End Workflow
911
-
912
- ## Overview
913
-
914
- This skill bridges two lexicons:
915
- - **\`@intentius/chant-lexicon-aws\`** — EKS cluster, node groups, IAM roles, OIDC provider (CloudFormation)
916
- - **\`@intentius/chant-lexicon-k8s\`** — Kubernetes workloads, IRSA, ALB Ingress, storage, observability (K8s YAML)
917
-
918
- ## Architecture
919
-
920
- \`\`\`
921
- AWS Lexicon (CloudFormation) K8s Lexicon (kubectl apply)
922
- ┌────────────────────────┐ ┌────────────────────────────┐
923
- │ VPC + Subnets │ │ NamespaceEnv (quotas) │
924
- │ EKS Cluster │ │ AutoscaledService (app) │
925
- │ Managed Node Group │──ARNs──→ │ IrsaServiceAccount (IRSA) │
926
- │ OIDC Provider │ │ AlbIngress (ALB) │
927
- │ IAM Roles (IRSA) │ │ EbsStorageClass (gp3) │
928
- │ EKS Add-ons │ │ FluentBitAgent (logs) │
929
- └────────────────────────┘ │ ExternalDnsAgent (DNS) │
930
- └────────────────────────────┘
931
- \`\`\`
932
-
933
- ## Step 1: Provision AWS Infrastructure
934
-
935
- \`\`\`bash
936
- # Build CloudFormation template
937
- chant build src/infra/ --output infra.json
938
-
939
- # Deploy
940
- aws cloudformation deploy \\
941
- --template-file infra.json \\
942
- --stack-name my-eks-cluster \\
943
- --capabilities CAPABILITY_NAMED_IAM
944
- \`\`\`
945
-
946
- Key AWS resources:
947
- - **EKS Cluster** — control plane
948
- - **Managed Node Group** — EC2 worker nodes
949
- - **OIDC Provider** — enables IRSA (IAM Roles for Service Accounts)
950
- - **IAM Roles** — node role, app IRSA roles, ALB controller role
951
-
952
- ## Step 2: Configure kubectl
953
-
954
- \`\`\`bash
955
- aws eks update-kubeconfig --name my-cluster --region us-east-1
956
- kubectl get nodes # verify connectivity
957
- \`\`\`
958
-
959
- ## Step 3: Deploy K8s Workloads
960
-
961
- \`\`\`bash
962
- # Build K8s manifests
963
- chant build src/k8s/ --output manifests.yaml
964
-
965
- # Apply
966
- kubectl apply -f manifests.yaml
967
- \`\`\`
968
-
969
- ### Key K8s composites for EKS
970
-
971
- \`\`\`typescript
972
- import {
973
- NamespaceEnv,
974
- AutoscaledService,
975
- IrsaServiceAccount,
976
- AlbIngress,
977
- EbsStorageClass,
978
- FluentBitAgent,
979
- ExternalDnsAgent,
980
- } from "@intentius/chant-lexicon-k8s";
981
-
982
- // 1. Namespace with quotas and network isolation
983
- const ns = NamespaceEnv({
984
- name: "prod",
985
- cpuQuota: "16",
986
- memoryQuota: "32Gi",
987
- defaultCpuRequest: "100m",
988
- defaultMemoryRequest: "128Mi",
989
- defaultDenyIngress: true,
990
- });
991
-
992
- // 2. IRSA ServiceAccount (use IAM Role ARN from CloudFormation outputs)
993
- const irsa = IrsaServiceAccount({
994
- name: "app-sa",
995
- iamRoleArn: "arn:aws:iam::123456789012:role/app-role", // from CF output
996
- namespace: "prod",
997
- });
998
-
999
- // 3. Application with autoscaling
1000
- const app = AutoscaledService({
1001
- name: "api",
1002
- image: "api:1.0",
1003
- port: 8080,
1004
- maxReplicas: 10,
1005
- cpuRequest: "200m",
1006
- memoryRequest: "256Mi",
1007
- namespace: "prod",
1008
- });
1009
-
1010
- // 4. ALB Ingress (use ACM cert ARN from CloudFormation outputs)
1011
- const ingress = AlbIngress({
1012
- name: "api-ingress",
1013
- hosts: [{ hostname: "api.example.com", paths: [{ path: "/", serviceName: "api", servicePort: 80 }] }],
1014
- certificateArn: "arn:aws:acm:us-east-1:123456789012:certificate/abc", // from CF output
1015
- namespace: "prod",
1016
- });
1017
-
1018
- // 5. Storage
1019
- const storage = EbsStorageClass({ name: "gp3-encrypted", type: "gp3", encrypted: true });
1020
-
1021
- // 6. Observability
1022
- const logging = FluentBitAgent({
1023
- logGroup: "/aws/eks/my-cluster/containers",
1024
- region: "us-east-1",
1025
- clusterName: "my-cluster",
1026
- });
1027
-
1028
- // 7. DNS
1029
- const dns = ExternalDnsAgent({
1030
- iamRoleArn: "arn:aws:iam::123456789012:role/external-dns-role",
1031
- domainFilters: ["example.com"],
1032
- });
1033
- \`\`\`
1034
-
1035
- ## Step 4: Verify
1036
-
1037
- \`\`\`bash
1038
- kubectl get pods -n prod
1039
- kubectl get ingress -n prod
1040
- kubectl logs -n amazon-cloudwatch -l app.kubernetes.io/name=fluent-bit
1041
- \`\`\`
1042
-
1043
- ## Cleanup
1044
-
1045
- \`\`\`bash
1046
- # Delete K8s workloads first
1047
- kubectl delete -f manifests.yaml
1048
-
1049
- # Then delete AWS infrastructure
1050
- aws cloudformation delete-stack --stack-name my-eks-cluster
1051
- aws cloudformation wait stack-delete-complete --stack-name my-eks-cluster
1052
- \`\`\`
1053
-
1054
- ## Cross-Lexicon Value Flow
1055
-
1056
- CloudFormation outputs flow into K8s composite props:
1057
-
1058
- | CloudFormation Output | K8s Composite Prop |
1059
- |----------------------|-------------------|
1060
- | App IAM Role ARN | \`IrsaServiceAccount.iamRoleArn\` |
1061
- | ALB Controller Role ARN | \`IrsaServiceAccount.iamRoleArn\` (for ALB controller SA) |
1062
- | ACM Certificate ARN | \`AlbIngress.certificateArn\` |
1063
- | ExternalDNS Role ARN | \`ExternalDnsAgent.iamRoleArn\` |
1064
- | EKS Cluster Name | \`FluentBitAgent.clusterName\`, \`AdotCollector.clusterName\` |
1065
- | EFS Filesystem ID | \`EfsStorageClass.fileSystemId\` |
1066
-
1067
- ## EKS Init Template
1068
-
1069
- Scaffold a dual-lexicon EKS project:
1070
-
1071
- \`\`\`bash
1072
- chant init --lexicon aws --template eks
1073
- \`\`\`
1074
-
1075
- This creates:
1076
- - \`src/infra/\` — EKS cluster, node group, IAM (AWS lexicon)
1077
- - \`src/k8s/\` — namespace, app, ingress, storage (K8s lexicon)
1078
- - \`package.json\` with both \`@intentius/chant-lexicon-aws\` and \`@intentius/chant-lexicon-k8s\`
1079
- `,
1080
915
  triggers: [
1081
916
  { type: "context", value: "eks" },
1082
917
  { type: "context", value: "kubernetes" },
1083
918
  { type: "context", value: "k8s-workloads" },
1084
919
  ],
1085
- preConditions: [
1086
- "AWS CLI is installed and configured",
1087
- "chant CLI is installed",
1088
- "kubectl is installed",
1089
- ],
1090
- postConditions: [
1091
- "EKS cluster is running",
1092
- "K8s workloads are deployed",
1093
- ],
1094
920
  parameters: [],
1095
921
  examples: [
1096
922
  {
1097
923
  title: "Full EKS deployment",
1098
- description: "Deploy infrastructure and workloads end-to-end",
1099
924
  input: "Set up a complete EKS environment with my API",
1100
- output: `# 1. Build and deploy infrastructure
1101
- chant build src/infra/ --output infra.json
1102
- aws cloudformation deploy --template-file infra.json --stack-name my-eks --capabilities CAPABILITY_NAMED_IAM
1103
-
1104
- # 2. Configure kubectl
1105
- aws eks update-kubeconfig --name my-cluster
1106
-
1107
- # 3. Build and deploy workloads
1108
- chant build src/k8s/ --output manifests.yaml
1109
- kubectl apply -f manifests.yaml`,
925
+ output: "chant build src/infra/ --output infra.json && aws cloudformation deploy --template-file infra.json --stack-name my-eks --capabilities CAPABILITY_NAMED_IAM",
1110
926
  },
1111
927
  ],
1112
928
  },
1113
929
  ];
930
+
931
+ for (const skill of skillFiles) {
932
+ try {
933
+ const content = readFileSync(join(dir, "skills", skill.file), "utf-8");
934
+ skills.push({
935
+ name: skill.name,
936
+ description: skill.description,
937
+ content,
938
+ triggers: skill.triggers,
939
+ parameters: skill.parameters,
940
+ examples: skill.examples,
941
+ });
942
+ } catch { /* skip missing skills */ }
943
+ }
944
+
945
+ return skills;
1114
946
  },
1115
947
 
1116
948
  completionProvider(ctx: CompletionContext): CompletionItem[] {
@@ -326,6 +326,46 @@ describe("LexiconOutput serialization", () => {
326
326
  });
327
327
  });
328
328
 
329
+ // ── StackOutput Serialization ──────────────────────────
330
+
331
+ describe("stackOutput serialization", () => {
332
+ test("Id attribute uses Ref (not Fn::GetAtt)", () => {
333
+ const bucket = new MockBucket({ BucketName: "my-bucket" });
334
+ const idRef = new AttrRef(bucket, "Id");
335
+ idRef._setLogicalName("MyBucket");
336
+
337
+ const output = stackOutput(idRef);
338
+
339
+ const entities = new Map<string, Declarable>();
340
+ entities.set("MyBucket", bucket);
341
+ entities.set("MyBucketId", output as unknown as Declarable);
342
+
343
+ const result = awsSerializer.serialize(entities);
344
+ const template = JSON.parse(result as string);
345
+
346
+ expect(template.Outputs.MyBucketId.Value).toEqual({ Ref: "MyBucket" });
347
+ });
348
+
349
+ test("non-Id attribute uses Fn::GetAtt", () => {
350
+ const bucket = new MockBucket({ BucketName: "my-bucket" });
351
+ const arnRef = new AttrRef(bucket, "Arn");
352
+ arnRef._setLogicalName("MyBucket");
353
+
354
+ const output = stackOutput(arnRef);
355
+
356
+ const entities = new Map<string, Declarable>();
357
+ entities.set("MyBucket", bucket);
358
+ entities.set("MyBucketArn", output as unknown as Declarable);
359
+
360
+ const result = awsSerializer.serialize(entities);
361
+ const template = JSON.parse(result as string);
362
+
363
+ expect(template.Outputs.MyBucketArn.Value).toEqual({
364
+ "Fn::GetAtt": ["MyBucket", "Arn"],
365
+ });
366
+ });
367
+ });
368
+
329
369
  // ── Nested Stack Serialization ──────────────────────────
330
370
 
331
371
  function mockChildBuildResult(childTemplate: object): BuildResult {
package/src/serializer.ts CHANGED
@@ -282,8 +282,13 @@ function serializeToTemplate(
282
282
  const ref = stackOutput.sourceRef;
283
283
  const logicalName = ref.getLogicalName();
284
284
  if (logicalName) {
285
+ // Use Ref for primary identifier ("Id") since not all resources
286
+ // support Fn::GetAtt for their primary identifier (e.g. ACM Certificate).
287
+ // Ref always returns the primary identifier for any CF resource.
285
288
  const output: CFOutput = {
286
- Value: { "Fn::GetAtt": [logicalName, ref.attribute] },
289
+ Value: ref.attribute === "Id"
290
+ ? { Ref: logicalName }
291
+ : { "Fn::GetAtt": [logicalName, ref.attribute] },
287
292
  };
288
293
  if (stackOutput.description) {
289
294
  output.Description = stackOutput.description;
File without changes