@intentius/chant-lexicon-gcp 0.0.15 → 0.0.18

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/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # @intentius/chant-lexicon-gcp
2
+
3
+ GCP Deployment Manager lexicon for [chant](https://intentius.io/chant/) — declare infrastructure as typed TypeScript that serializes to Deployment Manager YAML templates.
4
+
5
+ This package provides generated constructors for GCP resource types, type-safe template properties, pseudo-parameters, composites for grouping related resources, and GCP-specific lint rules. It also includes LSP and MCP server support for editor completions and hover.
6
+
7
+ ```bash
8
+ npm install --save-dev @intentius/chant @intentius/chant-lexicon-gcp
9
+ ```
10
+
11
+ **[Documentation →](https://intentius.io/chant/lexicons/gcp/)**
12
+
13
+ ## Related Packages
14
+
15
+ | Package | Role |
16
+ |---------|------|
17
+ | [@intentius/chant](https://www.npmjs.com/package/@intentius/chant) | Core type system, CLI, build pipeline |
18
+ | [@intentius/chant-lexicon-aws](https://www.npmjs.com/package/@intentius/chant-lexicon-aws) | AWS CloudFormation lexicon |
19
+ | [@intentius/chant-lexicon-azure](https://www.npmjs.com/package/@intentius/chant-lexicon-azure) | Azure ARM lexicon |
20
+
21
+ ## License
22
+
23
+ See the main project LICENSE file.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "xxhash64",
3
3
  "artifacts": {
4
- "manifest.json": "6cdf79b2aac2297c",
4
+ "manifest.json": "59d7c7c453ff4f80",
5
5
  "meta.json": "5c5359c023b36835",
6
6
  "types/index.d.ts": "c7cc45a739cefe9d",
7
7
  "rules/hardcoded-region.ts": "d230565c5cc6b600",
@@ -30,7 +30,8 @@
30
30
  "rules/wgc113.ts": "677724b28a9dbd5c",
31
31
  "skills/chant-gcp.md": "3c35bcda9067ed01",
32
32
  "skills/chant-gcp-security.md": "e93c103ba16b6d87",
33
- "skills/chant-gcp-patterns.md": "ab5dea252128c7d2"
33
+ "skills/chant-gcp-patterns.md": "ab5dea252128c7d2",
34
+ "skills/chant-gke.md": "15b6fd248379f66"
34
35
  },
35
- "composite": "83d2f9601b807e71"
36
+ "composite": "30b02f2993d30989"
36
37
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gcp",
3
- "version": "0.0.15",
3
+ "version": "0.0.18",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "GCP",
6
6
  "intrinsics": [],
@@ -0,0 +1,194 @@
1
+ ---
2
+ skill: chant-gke
3
+ description: End-to-end GKE workflow bridging GCP infrastructure and Kubernetes workloads
4
+ user-invocable: true
5
+ ---
6
+
7
+ # GKE End-to-End Workflow
8
+
9
+ ## Overview
10
+
11
+ This skill bridges two lexicons:
12
+ - **`@intentius/chant-lexicon-gcp`** — GKE cluster, node pool, service accounts, IAM bindings, Cloud DNS (Config Connector)
13
+ - **`@intentius/chant-lexicon-k8s`** — Kubernetes workloads, Workload Identity, GCE Ingress, storage, observability (K8s YAML)
14
+
15
+ ## Architecture
16
+
17
+ ```
18
+ GCP Lexicon (Config Connector) K8s Lexicon (kubectl apply)
19
+ ┌─────────────────────────────┐ ┌─────────────────────────────────┐
20
+ │ VPC + Subnets + Cloud NAT │ │ NamespaceEnv (quotas) │
21
+ │ GKE Cluster + Node Pool │ │ AutoscaledService (app) │
22
+ │ GCP Service Accounts │──GSA──→ │ WorkloadIdentityServiceAccount │
23
+ │ IAM Policy Members │ │ GCE Ingress + GkeExternalDns │
24
+ │ Cloud DNS Managed Zone │ │ GcePdStorageClass │
25
+ └─────────────────────────────┘ │ GkeFluentBitAgent (logs) │
26
+ │ GkeOtelCollector (traces) │
27
+ └─────────────────────────────────┘
28
+ ```
29
+
30
+ ## Step 1: Bootstrap (one-time)
31
+
32
+ Creates a GKE cluster with Config Connector enabled and configures Workload Identity:
33
+
34
+ ```bash
35
+ export GCP_PROJECT_ID=<your-project>
36
+ npm run bootstrap
37
+ ```
38
+
39
+ This enables required APIs, creates the cluster, sets up a Config Connector service account with editor/IAM/DNS roles, and waits for the controller to be ready.
40
+
41
+ ## Step 2: Build
42
+
43
+ ```bash
44
+ # Build Config Connector YAML
45
+ chant build src --lexicon gcp -o config.yaml
46
+
47
+ # Build K8s workload YAML
48
+ chant build src --lexicon k8s -o k8s.yaml
49
+ ```
50
+
51
+ Or use the combined script:
52
+
53
+ ```bash
54
+ npm run build
55
+ ```
56
+
57
+ ## Step 3: Deploy Config Connector Resources
58
+
59
+ ```bash
60
+ npm run deploy-infra
61
+ # Applies config.yaml — Config Connector reconciles GCP resources
62
+ ```
63
+
64
+ Key GCP resources created:
65
+ - **GKE Cluster** — control plane with Workload Identity enabled
66
+ - **Node Pool** — worker nodes
67
+ - **GCP Service Accounts** — app SA, ExternalDNS SA, logging SA, monitoring SA
68
+ - **IAM Policy Members** — Workload Identity bindings + role grants
69
+ - **Cloud DNS Managed Zone** — for ExternalDNS
70
+
71
+ ## Step 4: Load Outputs
72
+
73
+ ```bash
74
+ npm run load-outputs
75
+ ```
76
+
77
+ Populates `.env` with GCP service account emails and DNS zone info from the live cluster. These values flow into K8s composite props via `config.ts`.
78
+
79
+ ## Step 5: Deploy K8s Workloads
80
+
81
+ ```bash
82
+ npm run build:k8s # Rebuild with real values from .env
83
+ npm run apply # kubectl apply -f k8s.yaml
84
+ ```
85
+
86
+ ### Key K8s composites for GKE
87
+
88
+ ```typescript
89
+ import {
90
+ NamespaceEnv,
91
+ AutoscaledService,
92
+ WorkloadIdentityServiceAccount,
93
+ GkeExternalDnsAgent,
94
+ GcePdStorageClass,
95
+ GkeFluentBitAgent,
96
+ GkeOtelCollector,
97
+ } from "@intentius/chant-lexicon-k8s";
98
+
99
+ // 1. Namespace with quotas and network isolation
100
+ const ns = NamespaceEnv({
101
+ name: "prod",
102
+ cpuQuota: "16",
103
+ memoryQuota: "32Gi",
104
+ defaultCpuRequest: "100m",
105
+ defaultMemoryRequest: "128Mi",
106
+ defaultDenyIngress: true,
107
+ });
108
+
109
+ // 2. Workload Identity ServiceAccount (use GSA email from Config Connector outputs)
110
+ const wi = WorkloadIdentityServiceAccount({
111
+ name: "app-sa",
112
+ gcpServiceAccountEmail: "app@my-project.iam.gserviceaccount.com", // from .env
113
+ namespace: "prod",
114
+ });
115
+
116
+ // 3. Application with autoscaling
117
+ const app = AutoscaledService({
118
+ name: "api",
119
+ image: "api:1.0",
120
+ port: 8080,
121
+ maxReplicas: 10,
122
+ cpuRequest: "200m",
123
+ memoryRequest: "256Mi",
124
+ namespace: "prod",
125
+ });
126
+
127
+ // 4. ExternalDNS for Cloud DNS
128
+ const dns = GkeExternalDnsAgent({
129
+ gcpServiceAccountEmail: "dns@my-project.iam.gserviceaccount.com",
130
+ gcpProjectId: "my-project",
131
+ domainFilters: ["example.com"],
132
+ });
133
+
134
+ // 5. Storage
135
+ const storage = GcePdStorageClass({ name: "pd-balanced", type: "pd-balanced" });
136
+
137
+ // 6. Observability
138
+ const logging = GkeFluentBitAgent({
139
+ clusterName: "my-cluster",
140
+ projectId: "my-project",
141
+ });
142
+
143
+ const tracing = GkeOtelCollector({
144
+ clusterName: "my-cluster",
145
+ projectId: "my-project",
146
+ });
147
+ ```
148
+
149
+ ## Step 6: Verify
150
+
151
+ ```bash
152
+ npm run status
153
+ kubectl get pods -n prod
154
+ kubectl get ingress -n prod
155
+ kubectl logs -n gke-logging -l app.kubernetes.io/name=fluent-bit
156
+ ```
157
+
158
+ ## Cleanup
159
+
160
+ ```bash
161
+ npm run teardown
162
+ ```
163
+
164
+ Delete order matters:
165
+ 1. **K8s workloads** — drains load balancers
166
+ 2. **Config Connector resources** — deletes GCP infra (SAs, IAM, DNS)
167
+ 3. **Config Connector SA** — the CC controller's own SA
168
+ 4. **GKE cluster** — the bootstrap cluster itself
169
+
170
+ ## Cross-Lexicon Value Flow
171
+
172
+ Config Connector outputs flow into K8s composite props via `.env`:
173
+
174
+ | Config Connector Output | K8s Composite Prop |
175
+ |------------------------|-------------------|
176
+ | App GSA email | `WorkloadIdentityServiceAccount.gcpServiceAccountEmail` |
177
+ | ExternalDNS GSA email | `GkeExternalDnsAgent.gcpServiceAccountEmail` |
178
+ | Logging GSA email | `GkeFluentBitAgent.gcpServiceAccountEmail` |
179
+ | Monitoring GSA email | `GkeOtelCollector.gcpServiceAccountEmail` |
180
+ | GCP Project ID | `GkeExternalDnsAgent.gcpProjectId`, `GkeFluentBitAgent.projectId`, `GkeOtelCollector.projectId` |
181
+ | Cluster name | `GkeFluentBitAgent.clusterName`, `GkeOtelCollector.clusterName` |
182
+
183
+ ## GKE Init Template
184
+
185
+ Scaffold a dual-lexicon GKE project:
186
+
187
+ ```bash
188
+ chant init --lexicon gcp --template gke
189
+ ```
190
+
191
+ This creates:
192
+ - `src/infra/` — GKE cluster, node pool, service accounts, IAM (GCP lexicon)
193
+ - `src/k8s/` — namespace, app, ingress, storage (K8s lexicon)
194
+ - `package.json` with both `@intentius/chant-lexicon-gcp` and `@intentius/chant-lexicon-k8s`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-gcp",
3
- "version": "0.0.15",
3
+ "version": "0.0.18",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "files": [
@@ -25,7 +25,7 @@
25
25
  "prepack": "bun run generate && bun run bundle && bun run validate"
26
26
  },
27
27
  "dependencies": {
28
- "@intentius/chant": "0.0.15",
28
+ "@intentius/chant": "0.0.18",
29
29
  "fflate": "^0.8.2",
30
30
  "js-yaml": "^4.1.0"
31
31
  },
@@ -807,6 +807,9 @@ The \`chant-gcp\` skill covers:
807
807
  | \`examples/basic-bucket\` | Example StorageBucket code |`,
808
808
  },
809
809
  ],
810
+ sidebarExtra: [
811
+ { label: "Deploying to GKE", slug: "gke-kubernetes" },
812
+ ],
810
813
  };
811
814
 
812
815
  const result = docsPipeline(config);
@@ -109,7 +109,52 @@ const serviceAbbreviations: Record<string, string> = {
109
109
  const gcpNamingConfig: NamingConfig = {
110
110
  priorityNames,
111
111
  priorityAliases,
112
- priorityPropertyAliases: {},
112
+ priorityPropertyAliases: {
113
+ "GCP::Compute::Instance": {
114
+ NetworkInterface: "InstanceNetworkInterface",
115
+ AccessConfig: "InstanceAccessConfig",
116
+ AttachedDisk: "InstanceAttachedDisk",
117
+ Metadata: "InstanceMetadata",
118
+ Scheduling: "InstanceScheduling",
119
+ ServiceAccount: "InstanceServiceAccount",
120
+ ShieldedInstanceConfig: "InstanceShieldedConfig",
121
+ },
122
+ "GCP::Container::Cluster": {
123
+ NetworkConfig: "ClusterNetworkConfig",
124
+ NodeConfig: "ClusterNodeConfig",
125
+ IpAllocationPolicy: "ClusterIpAllocationPolicy",
126
+ MasterAuth: "ClusterMasterAuth",
127
+ PrivateClusterConfig: "ClusterPrivateConfig",
128
+ AddonsConfig: "ClusterAddonsConfig",
129
+ },
130
+ "GCP::Container::NodePool": {
131
+ NodeConfig: "NodePoolNodeConfig",
132
+ AutoScaling: "NodePoolAutoScaling",
133
+ Management: "NodePoolManagement",
134
+ UpgradeSettings: "NodePoolUpgradeSettings",
135
+ },
136
+ "GCP::Sql::Instance": {
137
+ IpConfiguration: "SqlIpConfiguration",
138
+ BackupConfiguration: "SqlBackupConfiguration",
139
+ DatabaseFlags: "SqlDatabaseFlags",
140
+ Settings: "SqlSettings",
141
+ },
142
+ "GCP::Storage::Bucket": {
143
+ Encryption: "BucketEncryption",
144
+ Versioning: "BucketVersioning",
145
+ Lifecycle: "BucketLifecycle",
146
+ LifecycleRule: "BucketLifecycleRule",
147
+ Logging: "BucketLogging",
148
+ RetentionPolicy: "BucketRetentionPolicy",
149
+ },
150
+ "GCP::Compute::Network": {
151
+ RoutingConfig: "NetworkRoutingConfig",
152
+ },
153
+ "GCP::Compute::Subnetwork": {
154
+ LogConfig: "SubnetworkLogConfig",
155
+ SecondaryIpRange: "SubnetworkSecondaryIpRange",
156
+ },
157
+ },
113
158
  serviceAbbreviations,
114
159
  shortName: gcpShortName,
115
160
  serviceName: (typeName: string) => {
@@ -40,6 +40,7 @@ spec:
40
40
  const diags = wgc101.check(makeCtx(yaml));
41
41
  expect(diags.length).toBeGreaterThanOrEqual(1);
42
42
  expect(diags[0].checkId).toBe("WGC101");
43
+ expect(diags[0].severity).toBe("warning");
43
44
  });
44
45
 
45
46
  test("no diagnostic when encryption present", () => {
@@ -73,6 +74,7 @@ spec:
73
74
  const diags = wgc102.check(makeCtx(yaml));
74
75
  expect(diags.length).toBeGreaterThanOrEqual(1);
75
76
  expect(diags[0].checkId).toBe("WGC102");
77
+ expect(diags[0].severity).toBe("warning");
76
78
  });
77
79
 
78
80
  test("no diagnostic when no public members", () => {
@@ -103,6 +105,7 @@ spec:
103
105
  const diags = wgc103.check(makeCtx(yaml));
104
106
  expect(diags.length).toBeGreaterThanOrEqual(1);
105
107
  expect(diags[0].checkId).toBe("WGC103");
108
+ expect(diags[0].severity).toBe("info");
106
109
  });
107
110
 
108
111
  test("no diagnostic when project annotation present", () => {
@@ -134,6 +137,7 @@ spec:
134
137
  const diags = wgc104.check(makeCtx(yaml));
135
138
  expect(diags.length).toBeGreaterThanOrEqual(1);
136
139
  expect(diags[0].checkId).toBe("WGC104");
140
+ expect(diags[0].severity).toBe("warning");
137
141
  });
138
142
 
139
143
  test("no diagnostic when uniformBucketLevelAccess is true", () => {
@@ -169,6 +173,7 @@ spec:
169
173
  const diags = wgc105.check(makeCtx(yaml));
170
174
  expect(diags.length).toBeGreaterThanOrEqual(1);
171
175
  expect(diags[0].checkId).toBe("WGC105");
176
+ expect(diags[0].severity).toBe("warning");
172
177
  });
173
178
 
174
179
  test("no diagnostic with private networks only", () => {
@@ -203,6 +208,7 @@ spec:
203
208
  const diags = wgc106.check(makeCtx(yaml));
204
209
  expect(diags.length).toBeGreaterThanOrEqual(1);
205
210
  expect(diags[0].checkId).toBe("WGC106");
211
+ expect(diags[0].severity).toBe("info");
206
212
  });
207
213
 
208
214
  test("no diagnostic when deletion policy present", () => {
@@ -234,6 +240,7 @@ spec:
234
240
  const diags = wgc107.check(makeCtx(yaml));
235
241
  expect(diags.length).toBeGreaterThanOrEqual(1);
236
242
  expect(diags[0].checkId).toBe("WGC107");
243
+ expect(diags[0].severity).toBe("info");
237
244
  });
238
245
 
239
246
  test("no diagnostic when versioning enabled", () => {
@@ -267,6 +274,7 @@ spec:
267
274
  const diags = wgc108.check(makeCtx(yaml));
268
275
  expect(diags.length).toBeGreaterThanOrEqual(1);
269
276
  expect(diags[0].checkId).toBe("WGC108");
277
+ expect(diags[0].severity).toBe("warning");
270
278
  });
271
279
 
272
280
  test("no diagnostic when backup enabled", () => {
@@ -305,6 +313,7 @@ spec:
305
313
  const diags = wgc109.check(makeCtx(yaml));
306
314
  expect(diags.length).toBeGreaterThanOrEqual(1);
307
315
  expect(diags[0].checkId).toBe("WGC109");
316
+ expect(diags[0].severity).toBe("warning");
308
317
  });
309
318
 
310
319
  test("no diagnostic with specific source range", () => {
@@ -339,6 +348,7 @@ spec:
339
348
  const diags = wgc110.check(makeCtx(yaml));
340
349
  expect(diags.length).toBeGreaterThanOrEqual(1);
341
350
  expect(diags[0].checkId).toBe("WGC110");
351
+ expect(diags[0].severity).toBe("warning");
342
352
  });
343
353
 
344
354
  test("no diagnostic when rotation period set", () => {
@@ -369,6 +379,7 @@ spec:
369
379
  const diags = wgc201.check(makeCtx(yaml));
370
380
  expect(diags.length).toBeGreaterThanOrEqual(1);
371
381
  expect(diags[0].checkId).toBe("WGC201");
382
+ expect(diags[0].severity).toBe("info");
372
383
  });
373
384
 
374
385
  test("no diagnostic when managed-by label present", () => {
@@ -400,6 +411,7 @@ spec:
400
411
  const diags = wgc202.check(makeCtx(yaml));
401
412
  expect(diags.length).toBeGreaterThanOrEqual(1);
402
413
  expect(diags[0].checkId).toBe("WGC202");
414
+ expect(diags[0].severity).toBe("warning");
403
415
  });
404
416
 
405
417
  test("no diagnostic when workload identity configured", () => {
@@ -435,6 +447,7 @@ spec:
435
447
  const diags = wgc203.check(makeCtx(yaml));
436
448
  expect(diags.length).toBeGreaterThanOrEqual(1);
437
449
  expect(diags[0].checkId).toBe("WGC203");
450
+ expect(diags[0].severity).toBe("warning");
438
451
  });
439
452
 
440
453
  test("no diagnostic with specific scopes", () => {
@@ -470,6 +483,7 @@ spec:
470
483
  const diags = wgc204.check(makeCtx(yaml));
471
484
  expect(diags.length).toBeGreaterThanOrEqual(1);
472
485
  expect(diags[0].checkId).toBe("WGC204");
486
+ expect(diags[0].severity).toBe("info");
473
487
  });
474
488
 
475
489
  test("no diagnostic when shielded VM configured", () => {
@@ -504,6 +518,7 @@ spec:
504
518
  const diags = wgc301.check(makeCtx(yaml));
505
519
  expect(diags.length).toBeGreaterThanOrEqual(1);
506
520
  expect(diags[0].checkId).toBe("WGC301");
521
+ expect(diags[0].severity).toBe("info");
507
522
  });
508
523
 
509
524
  test("no diagnostic when IAMAuditConfig present", () => {
@@ -536,6 +551,7 @@ spec:
536
551
  const diags = wgc302.check(makeCtx(yaml));
537
552
  expect(diags.length).toBeGreaterThanOrEqual(1);
538
553
  expect(diags[0].checkId).toBe("WGC302");
554
+ expect(diags[0].severity).toBe("info");
539
555
  });
540
556
 
541
557
  test("no diagnostic when Service resource present", () => {
@@ -565,6 +581,7 @@ spec:
565
581
  const diags = wgc303.check(makeCtx(yaml));
566
582
  expect(diags.length).toBeGreaterThanOrEqual(1);
567
583
  expect(diags[0].checkId).toBe("WGC303");
584
+ expect(diags[0].severity).toBe("info");
568
585
  });
569
586
 
570
587
  test("no diagnostic when service perimeter present", () => {
@@ -596,6 +613,7 @@ spec:
596
613
  const diags = wgc111.check(makeCtx(yaml));
597
614
  expect(diags.length).toBeGreaterThanOrEqual(1);
598
615
  expect(diags[0].checkId).toBe("WGC111");
616
+ expect(diags[0].severity).toBe("warning");
599
617
  });
600
618
 
601
619
  test("no diagnostic when referenced resource exists", () => {
package/src/plugin.ts CHANGED
@@ -322,6 +322,24 @@ export const bucket = new StorageBucket({
322
322
  },
323
323
  ],
324
324
  },
325
+ {
326
+ file: "chant-gke.md",
327
+ name: "chant-gke",
328
+ description: "GKE end-to-end workflow — bootstrap cluster, deploy Config Connector resources, deploy K8s workloads",
329
+ triggers: [
330
+ { type: "context" as const, value: "gke" },
331
+ { type: "context" as const, value: "gcp kubernetes" },
332
+ { type: "context" as const, value: "config connector" },
333
+ ],
334
+ parameters: [],
335
+ examples: [
336
+ {
337
+ title: "Deploy GKE microservice",
338
+ input: "Deploy a GKE project end-to-end",
339
+ output: "npm run bootstrap && npm run deploy",
340
+ },
341
+ ],
342
+ },
325
343
  ];
326
344
 
327
345
  for (const skill of skillFiles) {
@@ -7,6 +7,7 @@ import {
7
7
  DEFAULT_LABELS_MARKER,
8
8
  DEFAULT_ANNOTATIONS_MARKER,
9
9
  } from "./default-labels";
10
+ import { GCP } from "./pseudo";
10
11
 
11
12
  // ── Mock helpers ────────────────────────────────────────────────────
12
13
 
@@ -160,6 +161,50 @@ describe("gcpSerializer", () => {
160
161
  expect(result).toContain("cnrm.cloud.google.com/project-id: my-project");
161
162
  });
162
163
 
164
+ test("PseudoParameter in default annotations resolves to env var string", () => {
165
+ const prev = process.env.GCP_PROJECT_ID;
166
+ process.env.GCP_PROJECT_ID = "test-project-123";
167
+ try {
168
+ const entities = new Map<string, any>();
169
+ entities.set("annot", defaultAnnotations({ "cnrm.cloud.google.com/project-id": GCP.ProjectId }));
170
+ entities.set(
171
+ "bucket",
172
+ mockResource("GCP::Storage::Bucket", {
173
+ location: "US",
174
+ }),
175
+ );
176
+
177
+ const result = gcpSerializer.serialize(entities);
178
+ expect(result).toContain("cnrm.cloud.google.com/project-id: test-project-123");
179
+ expect(result).not.toContain("refName");
180
+ expect(result).not.toContain("Ref");
181
+ } finally {
182
+ if (prev === undefined) delete process.env.GCP_PROJECT_ID;
183
+ else process.env.GCP_PROJECT_ID = prev;
184
+ }
185
+ });
186
+
187
+ test("PseudoParameter in default annotations falls back when env var unset", () => {
188
+ const prev = process.env.GCP_PROJECT_ID;
189
+ delete process.env.GCP_PROJECT_ID;
190
+ try {
191
+ const entities = new Map<string, any>();
192
+ entities.set("annot", defaultAnnotations({ "cnrm.cloud.google.com/project-id": GCP.ProjectId }));
193
+ entities.set(
194
+ "bucket",
195
+ mockResource("GCP::Storage::Bucket", {
196
+ location: "US",
197
+ }),
198
+ );
199
+
200
+ const result = gcpSerializer.serialize(entities);
201
+ expect(result).toContain("cnrm.cloud.google.com/project-id: PROJECT_ID");
202
+ } finally {
203
+ if (prev === undefined) delete process.env.GCP_PROJECT_ID;
204
+ else process.env.GCP_PROJECT_ID = prev;
205
+ }
206
+ });
207
+
163
208
  test("explicit labels override default labels", () => {
164
209
  const entities = new Map<string, any>();
165
210
  entities.set("labels", defaultLabels({ env: "dev" }));
package/src/serializer.ts CHANGED
@@ -12,6 +12,7 @@ import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
12
12
  import type { LexiconOutput } from "@intentius/chant/lexicon-output";
13
13
  import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
14
14
  import { emitYAML } from "@intentius/chant/yaml";
15
+ import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
15
16
  import { isDefaultLabels, isDefaultAnnotations, type DefaultLabels, type DefaultAnnotations } from "./default-labels";
16
17
 
17
18
  const require = createRequire(import.meta.url);
@@ -170,10 +171,14 @@ export const gcpSerializer: Serializer = {
170
171
  metadata.labels = { ...defaultLabelEntries, ...existingLabels };
171
172
  }
172
173
 
173
- // Merge default annotations
174
+ // Merge default annotations (resolve PseudoParameters to env-var strings)
174
175
  if (Object.keys(defaultAnnotationEntries).length > 0) {
176
+ const resolvedAnnotations: Record<string, unknown> = {};
177
+ for (const [k, v] of Object.entries(defaultAnnotationEntries)) {
178
+ resolvedAnnotations[k] = resolveAnnotationValue(v);
179
+ }
175
180
  const existingAnnotations = (metadata.annotations ?? {}) as Record<string, unknown>;
176
- metadata.annotations = { ...defaultAnnotationEntries, ...existingAnnotations };
181
+ metadata.annotations = { ...resolvedAnnotations, ...existingAnnotations };
177
182
  }
178
183
 
179
184
  manifest.metadata = metadata;
@@ -197,6 +202,38 @@ export const gcpSerializer: Serializer = {
197
202
  },
198
203
  };
199
204
 
205
+ /**
206
+ * Pseudo-parameter → environment variable mapping.
207
+ * Config Connector annotations must be plain strings, so PseudoParameters
208
+ * are resolved from environment variables at build time.
209
+ */
210
+ const PSEUDO_ENV_MAP: Record<string, { envVar: string; fallback: string }> = {
211
+ "GCP::ProjectId": { envVar: "GCP_PROJECT_ID", fallback: "PROJECT_ID" },
212
+ "GCP::Region": { envVar: "GCP_REGION", fallback: "us-central1" },
213
+ "GCP::Zone": { envVar: "GCP_ZONE", fallback: "us-central1-a" },
214
+ };
215
+
216
+ /**
217
+ * Resolve an annotation value to a plain string.
218
+ * PseudoParameter intrinsics are resolved from environment variables;
219
+ * other values pass through unchanged.
220
+ */
221
+ function resolveAnnotationValue(value: unknown): unknown {
222
+ if (typeof value === "object" && value !== null && INTRINSIC_MARKER in value) {
223
+ if ("toJSON" in value && typeof value.toJSON === "function") {
224
+ const json = value.toJSON() as Record<string, unknown>;
225
+ if (json && typeof json === "object" && "Ref" in json && typeof json.Ref === "string") {
226
+ const mapping = PSEUDO_ENV_MAP[json.Ref];
227
+ if (mapping) {
228
+ return process.env[mapping.envVar] ?? mapping.fallback;
229
+ }
230
+ }
231
+ }
232
+ return String(value);
233
+ }
234
+ return value;
235
+ }
236
+
200
237
  /**
201
238
  * Emit a key-value pair as YAML.
202
239
  */
@@ -0,0 +1,194 @@
1
+ ---
2
+ skill: chant-gke
3
+ description: End-to-end GKE workflow bridging GCP infrastructure and Kubernetes workloads
4
+ user-invocable: true
5
+ ---
6
+
7
+ # GKE End-to-End Workflow
8
+
9
+ ## Overview
10
+
11
+ This skill bridges two lexicons:
12
+ - **`@intentius/chant-lexicon-gcp`** — GKE cluster, node pool, service accounts, IAM bindings, Cloud DNS (Config Connector)
13
+ - **`@intentius/chant-lexicon-k8s`** — Kubernetes workloads, Workload Identity, GCE Ingress, storage, observability (K8s YAML)
14
+
15
+ ## Architecture
16
+
17
+ ```
18
+ GCP Lexicon (Config Connector) K8s Lexicon (kubectl apply)
19
+ ┌─────────────────────────────┐ ┌─────────────────────────────────┐
20
+ │ VPC + Subnets + Cloud NAT │ │ NamespaceEnv (quotas) │
21
+ │ GKE Cluster + Node Pool │ │ AutoscaledService (app) │
22
+ │ GCP Service Accounts │──GSA──→ │ WorkloadIdentityServiceAccount │
23
+ │ IAM Policy Members │ │ GCE Ingress + GkeExternalDns │
24
+ │ Cloud DNS Managed Zone │ │ GcePdStorageClass │
25
+ └─────────────────────────────┘ │ GkeFluentBitAgent (logs) │
26
+ │ GkeOtelCollector (traces) │
27
+ └─────────────────────────────────┘
28
+ ```
29
+
30
+ ## Step 1: Bootstrap (one-time)
31
+
32
+ Creates a GKE cluster with Config Connector enabled and configures Workload Identity:
33
+
34
+ ```bash
35
+ export GCP_PROJECT_ID=<your-project>
36
+ npm run bootstrap
37
+ ```
38
+
39
+ This enables required APIs, creates the cluster, sets up a Config Connector service account with editor/IAM/DNS roles, and waits for the controller to be ready.
40
+
41
+ ## Step 2: Build
42
+
43
+ ```bash
44
+ # Build Config Connector YAML
45
+ chant build src --lexicon gcp -o config.yaml
46
+
47
+ # Build K8s workload YAML
48
+ chant build src --lexicon k8s -o k8s.yaml
49
+ ```
50
+
51
+ Or use the combined script:
52
+
53
+ ```bash
54
+ npm run build
55
+ ```
56
+
57
+ ## Step 3: Deploy Config Connector Resources
58
+
59
+ ```bash
60
+ npm run deploy-infra
61
+ # Applies config.yaml — Config Connector reconciles GCP resources
62
+ ```
63
+
64
+ Key GCP resources created:
65
+ - **GKE Cluster** — control plane with Workload Identity enabled
66
+ - **Node Pool** — worker nodes
67
+ - **GCP Service Accounts** — app SA, ExternalDNS SA, logging SA, monitoring SA
68
+ - **IAM Policy Members** — Workload Identity bindings + role grants
69
+ - **Cloud DNS Managed Zone** — for ExternalDNS
70
+
71
+ ## Step 4: Load Outputs
72
+
73
+ ```bash
74
+ npm run load-outputs
75
+ ```
76
+
77
+ Populates `.env` with GCP service account emails and DNS zone info from the live cluster. These values flow into K8s composite props via `config.ts`.
78
+
79
+ ## Step 5: Deploy K8s Workloads
80
+
81
+ ```bash
82
+ npm run build:k8s # Rebuild with real values from .env
83
+ npm run apply # kubectl apply -f k8s.yaml
84
+ ```
85
+
86
+ ### Key K8s composites for GKE
87
+
88
+ ```typescript
89
+ import {
90
+ NamespaceEnv,
91
+ AutoscaledService,
92
+ WorkloadIdentityServiceAccount,
93
+ GkeExternalDnsAgent,
94
+ GcePdStorageClass,
95
+ GkeFluentBitAgent,
96
+ GkeOtelCollector,
97
+ } from "@intentius/chant-lexicon-k8s";
98
+
99
+ // 1. Namespace with quotas and network isolation
100
+ const ns = NamespaceEnv({
101
+ name: "prod",
102
+ cpuQuota: "16",
103
+ memoryQuota: "32Gi",
104
+ defaultCpuRequest: "100m",
105
+ defaultMemoryRequest: "128Mi",
106
+ defaultDenyIngress: true,
107
+ });
108
+
109
+ // 2. Workload Identity ServiceAccount (use GSA email from Config Connector outputs)
110
+ const wi = WorkloadIdentityServiceAccount({
111
+ name: "app-sa",
112
+ gcpServiceAccountEmail: "app@my-project.iam.gserviceaccount.com", // from .env
113
+ namespace: "prod",
114
+ });
115
+
116
+ // 3. Application with autoscaling
117
+ const app = AutoscaledService({
118
+ name: "api",
119
+ image: "api:1.0",
120
+ port: 8080,
121
+ maxReplicas: 10,
122
+ cpuRequest: "200m",
123
+ memoryRequest: "256Mi",
124
+ namespace: "prod",
125
+ });
126
+
127
+ // 4. ExternalDNS for Cloud DNS
128
+ const dns = GkeExternalDnsAgent({
129
+ gcpServiceAccountEmail: "dns@my-project.iam.gserviceaccount.com",
130
+ gcpProjectId: "my-project",
131
+ domainFilters: ["example.com"],
132
+ });
133
+
134
+ // 5. Storage
135
+ const storage = GcePdStorageClass({ name: "pd-balanced", type: "pd-balanced" });
136
+
137
+ // 6. Observability
138
+ const logging = GkeFluentBitAgent({
139
+ clusterName: "my-cluster",
140
+ projectId: "my-project",
141
+ });
142
+
143
+ const tracing = GkeOtelCollector({
144
+ clusterName: "my-cluster",
145
+ projectId: "my-project",
146
+ });
147
+ ```
148
+
149
+ ## Step 6: Verify
150
+
151
+ ```bash
152
+ npm run status
153
+ kubectl get pods -n prod
154
+ kubectl get ingress -n prod
155
+ kubectl logs -n gke-logging -l app.kubernetes.io/name=fluent-bit
156
+ ```
157
+
158
+ ## Cleanup
159
+
160
+ ```bash
161
+ npm run teardown
162
+ ```
163
+
164
+ Delete order matters:
165
+ 1. **K8s workloads** — drains load balancers
166
+ 2. **Config Connector resources** — deletes GCP infra (SAs, IAM, DNS)
167
+ 3. **Config Connector SA** — the CC controller's own SA
168
+ 4. **GKE cluster** — the bootstrap cluster itself
169
+
170
+ ## Cross-Lexicon Value Flow
171
+
172
+ Config Connector outputs flow into K8s composite props via `.env`:
173
+
174
+ | Config Connector Output | K8s Composite Prop |
175
+ |------------------------|-------------------|
176
+ | App GSA email | `WorkloadIdentityServiceAccount.gcpServiceAccountEmail` |
177
+ | ExternalDNS GSA email | `GkeExternalDnsAgent.gcpServiceAccountEmail` |
178
+ | Logging GSA email | `GkeFluentBitAgent.gcpServiceAccountEmail` |
179
+ | Monitoring GSA email | `GkeOtelCollector.gcpServiceAccountEmail` |
180
+ | GCP Project ID | `GkeExternalDnsAgent.gcpProjectId`, `GkeFluentBitAgent.projectId`, `GkeOtelCollector.projectId` |
181
+ | Cluster name | `GkeFluentBitAgent.clusterName`, `GkeOtelCollector.clusterName` |
182
+
183
+ ## GKE Init Template
184
+
185
+ Scaffold a dual-lexicon GKE project:
186
+
187
+ ```bash
188
+ chant init --lexicon gcp --template gke
189
+ ```
190
+
191
+ This creates:
192
+ - `src/infra/` — GKE cluster, node pool, service accounts, IAM (GCP lexicon)
193
+ - `src/k8s/` — namespace, app, ingress, storage (K8s lexicon)
194
+ - `package.json` with both `@intentius/chant-lexicon-gcp` and `@intentius/chant-lexicon-k8s`