@intentius/chant-lexicon-gcp 0.0.15 → 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/README.md +23 -0
- package/dist/integrity.json +4 -3
- package/dist/manifest.json +1 -1
- package/dist/skills/chant-gke.md +194 -0
- package/package.json +1 -1
- package/src/codegen/naming.ts +46 -1
- package/src/lint/post-synth/post-synth.test.ts +18 -0
- package/src/plugin.ts +18 -0
- package/src/serializer.test.ts +45 -0
- package/src/serializer.ts +39 -2
- package/src/skills/chant-gke.md +194 -0
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.
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "xxhash64",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "ad77fb29e1f765b8",
|
|
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": "
|
|
36
|
+
"composite": "ffdc5245f6535b62"
|
|
36
37
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -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
package/src/codegen/naming.ts
CHANGED
|
@@ -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) {
|
package/src/serializer.test.ts
CHANGED
|
@@ -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 = { ...
|
|
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`
|