@intentius/chant-lexicon-gcp 0.0.15

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.
Files changed (122) hide show
  1. package/dist/integrity.json +36 -0
  2. package/dist/manifest.json +12 -0
  3. package/dist/meta.json +10919 -0
  4. package/dist/rules/gcp-helpers.ts +117 -0
  5. package/dist/rules/hardcoded-project.ts +58 -0
  6. package/dist/rules/hardcoded-region.ts +56 -0
  7. package/dist/rules/public-iam.ts +43 -0
  8. package/dist/rules/wgc101.ts +56 -0
  9. package/dist/rules/wgc102.ts +35 -0
  10. package/dist/rules/wgc103.ts +45 -0
  11. package/dist/rules/wgc104.ts +42 -0
  12. package/dist/rules/wgc105.ts +46 -0
  13. package/dist/rules/wgc106.ts +36 -0
  14. package/dist/rules/wgc107.ts +39 -0
  15. package/dist/rules/wgc108.ts +41 -0
  16. package/dist/rules/wgc109.ts +39 -0
  17. package/dist/rules/wgc110.ts +38 -0
  18. package/dist/rules/wgc111.ts +54 -0
  19. package/dist/rules/wgc112.ts +56 -0
  20. package/dist/rules/wgc113.ts +42 -0
  21. package/dist/rules/wgc201.ts +36 -0
  22. package/dist/rules/wgc202.ts +39 -0
  23. package/dist/rules/wgc203.ts +44 -0
  24. package/dist/rules/wgc204.ts +39 -0
  25. package/dist/rules/wgc301.ts +34 -0
  26. package/dist/rules/wgc302.ts +34 -0
  27. package/dist/rules/wgc303.ts +37 -0
  28. package/dist/skills/chant-gcp-patterns.md +367 -0
  29. package/dist/skills/chant-gcp-security.md +276 -0
  30. package/dist/skills/chant-gcp.md +108 -0
  31. package/dist/types/index.d.ts +26529 -0
  32. package/package.json +35 -0
  33. package/src/actions/index.ts +52 -0
  34. package/src/codegen/docs-cli.ts +7 -0
  35. package/src/codegen/docs.ts +820 -0
  36. package/src/codegen/generate-cli.ts +24 -0
  37. package/src/codegen/generate.ts +252 -0
  38. package/src/codegen/naming.test.ts +49 -0
  39. package/src/codegen/naming.ts +132 -0
  40. package/src/codegen/package.ts +66 -0
  41. package/src/composites/cloud-function.ts +117 -0
  42. package/src/composites/cloud-run-service.ts +124 -0
  43. package/src/composites/cloud-sql-instance.ts +126 -0
  44. package/src/composites/composites.test.ts +432 -0
  45. package/src/composites/gcs-bucket.ts +111 -0
  46. package/src/composites/gke-cluster.ts +125 -0
  47. package/src/composites/index.ts +20 -0
  48. package/src/composites/managed-certificate.ts +79 -0
  49. package/src/composites/private-service.ts +95 -0
  50. package/src/composites/pubsub-pipeline.ts +102 -0
  51. package/src/composites/secure-project.ts +128 -0
  52. package/src/composites/vpc-network.ts +165 -0
  53. package/src/coverage.test.ts +27 -0
  54. package/src/coverage.ts +51 -0
  55. package/src/default-labels.test.ts +111 -0
  56. package/src/default-labels.ts +93 -0
  57. package/src/generated/index.d.ts +26529 -0
  58. package/src/generated/index.ts +1723 -0
  59. package/src/generated/lexicon-gcp.json +10919 -0
  60. package/src/generated/runtime.ts +4 -0
  61. package/src/import/generator.test.ts +125 -0
  62. package/src/import/generator.ts +82 -0
  63. package/src/import/parser.test.ts +167 -0
  64. package/src/import/parser.ts +80 -0
  65. package/src/import/roundtrip.test.ts +66 -0
  66. package/src/index.ts +54 -0
  67. package/src/lint/post-synth/gcp-helpers.ts +117 -0
  68. package/src/lint/post-synth/index.ts +20 -0
  69. package/src/lint/post-synth/post-synth.test.ts +693 -0
  70. package/src/lint/post-synth/wgc101.ts +56 -0
  71. package/src/lint/post-synth/wgc102.ts +35 -0
  72. package/src/lint/post-synth/wgc103.ts +45 -0
  73. package/src/lint/post-synth/wgc104.ts +42 -0
  74. package/src/lint/post-synth/wgc105.ts +46 -0
  75. package/src/lint/post-synth/wgc106.ts +36 -0
  76. package/src/lint/post-synth/wgc107.ts +39 -0
  77. package/src/lint/post-synth/wgc108.ts +41 -0
  78. package/src/lint/post-synth/wgc109.ts +39 -0
  79. package/src/lint/post-synth/wgc110.ts +38 -0
  80. package/src/lint/post-synth/wgc111.ts +54 -0
  81. package/src/lint/post-synth/wgc112.ts +56 -0
  82. package/src/lint/post-synth/wgc113.ts +42 -0
  83. package/src/lint/post-synth/wgc201.ts +36 -0
  84. package/src/lint/post-synth/wgc202.ts +39 -0
  85. package/src/lint/post-synth/wgc203.ts +44 -0
  86. package/src/lint/post-synth/wgc204.ts +39 -0
  87. package/src/lint/post-synth/wgc301.ts +34 -0
  88. package/src/lint/post-synth/wgc302.ts +34 -0
  89. package/src/lint/post-synth/wgc303.ts +37 -0
  90. package/src/lint/rules/hardcoded-project.ts +58 -0
  91. package/src/lint/rules/hardcoded-region.ts +56 -0
  92. package/src/lint/rules/index.ts +3 -0
  93. package/src/lint/rules/public-iam.ts +43 -0
  94. package/src/lint/rules/rules.test.ts +63 -0
  95. package/src/lsp/completions.test.ts +67 -0
  96. package/src/lsp/completions.ts +17 -0
  97. package/src/lsp/hover.test.ts +66 -0
  98. package/src/lsp/hover.ts +54 -0
  99. package/src/package-cli.ts +24 -0
  100. package/src/plugin.test.ts +250 -0
  101. package/src/plugin.ts +405 -0
  102. package/src/pseudo.test.ts +40 -0
  103. package/src/pseudo.ts +19 -0
  104. package/src/serializer.test.ts +250 -0
  105. package/src/serializer.ts +232 -0
  106. package/src/skills/chant-gcp-patterns.md +367 -0
  107. package/src/skills/chant-gcp-security.md +276 -0
  108. package/src/skills/chant-gcp.md +108 -0
  109. package/src/spec/fetch.test.ts +16 -0
  110. package/src/spec/fetch.ts +121 -0
  111. package/src/spec/parse.test.ts +163 -0
  112. package/src/spec/parse.ts +432 -0
  113. package/src/testdata/compute-instance.yaml +93 -0
  114. package/src/testdata/iam-policy-member.yaml +66 -0
  115. package/src/testdata/manifests/compute-instance.yaml +18 -0
  116. package/src/testdata/manifests/full-app.yaml +34 -0
  117. package/src/testdata/manifests/storage-bucket.yaml +12 -0
  118. package/src/testdata/storage-bucket.yaml +100 -0
  119. package/src/validate-cli.ts +13 -0
  120. package/src/validate.test.ts +38 -0
  121. package/src/validate.ts +30 -0
  122. package/src/variables.ts +15 -0
@@ -0,0 +1,108 @@
1
+ ---
2
+ skill: chant-gcp
3
+ description: Build, validate, and deploy GCP Config Connector manifests from a chant project
4
+ source: chant-lexicon
5
+ user-invocable: true
6
+ ---
7
+
8
+ # GCP Config Connector Operational Playbook
9
+
10
+ ## How chant and Config Connector relate
11
+
12
+ chant is a **synthesis-only** tool — it compiles TypeScript source files into Config Connector YAML manifests. chant does NOT call GCP APIs. Your job as an agent is to bridge the two:
13
+
14
+ - Use **chant** for: build, lint, diff (local YAML comparison)
15
+ - Use **kubectl** for: apply, rollback, monitoring, troubleshooting
16
+
17
+ The source of truth for infrastructure is the TypeScript in `src/`. The generated YAML manifests are intermediate artifacts.
18
+
19
+ ## Prerequisites
20
+
21
+ 1. A GKE cluster with Config Connector installed
22
+ 2. A ConfigConnectorContext resource per namespace
23
+ 3. A GCP Service Account with appropriate IAM roles
24
+
25
+ ## Build and validate
26
+
27
+ ### Build manifests
28
+
29
+ ```bash
30
+ chant build src/ --output manifests.yaml
31
+ ```
32
+
33
+ ### Lint the source
34
+
35
+ ```bash
36
+ chant lint src/
37
+ ```
38
+
39
+ ### What each step catches
40
+
41
+ | Step | Catches | When to run |
42
+ |------|---------|-------------|
43
+ | `chant lint` | Hardcoded project IDs (WGC001), regions (WGC002), public IAM (WGC003) | Every edit |
44
+ | `chant build` | Post-synth: missing encryption (WGC101), public IAM in output (WGC102), missing project annotation (WGC103) | Before apply |
45
+
46
+ ## Applying to Kubernetes
47
+
48
+ ```bash
49
+ # Build
50
+ chant build src/ --output manifests.yaml
51
+
52
+ # Dry run
53
+ kubectl apply -f manifests.yaml --dry-run=server
54
+
55
+ # Apply
56
+ kubectl apply -f manifests.yaml
57
+ ```
58
+
59
+ ## Resource reference patterns
60
+
61
+ Config Connector resources reference each other using `resourceRef`:
62
+
63
+ ```yaml
64
+ # By name (same namespace)
65
+ resourceRef:
66
+ name: my-network
67
+
68
+ # By external reference (cross-project)
69
+ resourceRef:
70
+ external: projects/my-project/global/networks/my-network
71
+ ```
72
+
73
+ ## Project binding
74
+
75
+ Bind resources to a GCP project via annotations:
76
+
77
+ ```yaml
78
+ metadata:
79
+ annotations:
80
+ cnrm.cloud.google.com/project-id: my-project
81
+ ```
82
+
83
+ Or use defaultAnnotations in chant:
84
+
85
+ ```typescript
86
+ export const annotations = defaultAnnotations({
87
+ "cnrm.cloud.google.com/project-id": GCP.ProjectId,
88
+ });
89
+ ```
90
+
91
+ ## Troubleshooting
92
+
93
+ | Status | Meaning | Fix |
94
+ |--------|---------|-----|
95
+ | UpToDate | Resource is in sync | None needed |
96
+ | UpdateFailed | GCP API error | Check `kubectl describe` events |
97
+ | DependencyNotReady | Waiting for referenced resource | Ensure dependency exists |
98
+ | DeletionFailed | Cannot delete GCP resource | Check IAM permissions |
99
+
100
+ ## Quick reference
101
+
102
+ | Command | Description |
103
+ |---------|-------------|
104
+ | `chant build src/` | Synthesize manifests |
105
+ | `chant lint src/` | Check for anti-patterns |
106
+ | `kubectl apply -f manifests.yaml` | Apply to cluster |
107
+ | `kubectl get gcp` | List all Config Connector resources |
108
+ | `kubectl describe <resource>` | Check reconciliation status |
@@ -0,0 +1,16 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { getCachePath, KCC_VERSION } from "./fetch";
3
+
4
+ describe("fetch", () => {
5
+ test("cache path includes version", () => {
6
+ const path = getCachePath();
7
+ expect(path).toContain(KCC_VERSION);
8
+ expect(path).toContain(".chant");
9
+ expect(path).toEndWith(".tar.gz");
10
+ });
11
+
12
+ test("custom version in cache path", () => {
13
+ const path = getCachePath("v1.120.0");
14
+ expect(path).toContain("v1.120.0");
15
+ });
16
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Schema fetching — downloads Config Connector CRD bundle from GitHub releases.
3
+ *
4
+ * Uses the release bundle tar.gz, filtering for CRD YAML files.
5
+ */
6
+
7
+ import { homedir } from "os";
8
+ import { join } from "path";
9
+ import { fetchWithCache, clearCacheFile } from "@intentius/chant/codegen/fetch";
10
+
11
+ /**
12
+ * Pinned Config Connector version for reproducible codegen.
13
+ */
14
+ export const KCC_VERSION = "v1.145.0";
15
+
16
+ const CACHE_DIR = join(homedir(), ".chant");
17
+
18
+ function bundleUrl(version: string): string {
19
+ // The release-bundle.tar.gz asset is no longer published.
20
+ // Use GitHub's auto-generated source tarball instead — CRDs are at config/crds/resources/.
21
+ return `https://github.com/GoogleCloudPlatform/k8s-config-connector/archive/refs/tags/${version}.tar.gz`;
22
+ }
23
+
24
+ function cacheFile(version: string): string {
25
+ return join(CACHE_DIR, `kcc-crds-${version}.tar.gz`);
26
+ }
27
+
28
+ /**
29
+ * Fetch the Config Connector CRD bundle and extract CRD YAML files.
30
+ *
31
+ * Returns a Map keyed by CRD filename (e.g., "computeinstance.yaml") with
32
+ * the raw YAML content as a Buffer.
33
+ */
34
+ export async function fetchCRDBundle(
35
+ force = false,
36
+ version: string = KCC_VERSION,
37
+ ): Promise<Map<string, Buffer>> {
38
+ const url = bundleUrl(version);
39
+ const cache = cacheFile(version);
40
+
41
+ const tarData = await fetchWithCache({ url, cacheFile: cache }, force);
42
+ return extractCRDs(tarData);
43
+ }
44
+
45
+ /**
46
+ * Extract CRD YAML files from a tar.gz bundle.
47
+ *
48
+ * Filters for entries under a crds/ directory or standalone CRD YAML files.
49
+ */
50
+ async function extractCRDs(tarData: Buffer): Promise<Map<string, Buffer>> {
51
+ const { gunzipSync } = await import("fflate");
52
+ const crds = new Map<string, Buffer>();
53
+
54
+ // Decompress gzip synchronously (avoids lingering event loop references)
55
+ const gunzipped = gunzipSync(new Uint8Array(tarData));
56
+
57
+ // Parse tar (512-byte header blocks) with PAX extended header support.
58
+ // GitHub source tarballs use PAX headers (type 'x') for long filenames.
59
+ let offset = 0;
60
+ const data = gunzipped;
61
+ let paxPath: string | null = null;
62
+
63
+ while (offset < data.length - 512) {
64
+ const header = data.slice(offset, offset + 512);
65
+ offset += 512;
66
+
67
+ if (header.every((b) => b === 0)) break;
68
+
69
+ // Extract filename: ustar prefix (bytes 345-499) + name (bytes 0-99)
70
+ const nameBytes = header.slice(0, 100);
71
+ const nameEnd = nameBytes.indexOf(0);
72
+ let name = new TextDecoder().decode(nameBytes.slice(0, nameEnd > 0 ? nameEnd : 100)).trim();
73
+ const prefixBytes = header.slice(345, 500);
74
+ const prefixEnd = prefixBytes.indexOf(0);
75
+ const ustarPrefix = new TextDecoder().decode(prefixBytes.slice(0, prefixEnd > 0 ? prefixEnd : 155)).trim();
76
+ if (ustarPrefix) name = `${ustarPrefix}/${name}`;
77
+
78
+ const sizeStr = new TextDecoder().decode(header.slice(124, 136)).trim().replace(/\0/g, "");
79
+ const size = parseInt(sizeStr, 8) || 0;
80
+ const typeFlag = header[156];
81
+ const typeFlagChar = String.fromCharCode(typeFlag);
82
+
83
+ const blocks = Math.ceil(size / 512);
84
+ const fileData = data.slice(offset, offset + size);
85
+ offset += blocks * 512;
86
+
87
+ // PAX extended header — extract path= for the next entry
88
+ if (typeFlagChar === "x") {
89
+ const paxStr = new TextDecoder().decode(fileData);
90
+ const match = paxStr.match(/\bpath=([^\n]+)/);
91
+ if (match) paxPath = match[1];
92
+ continue;
93
+ }
94
+
95
+ // Use PAX path if available, then reset
96
+ if (paxPath) {
97
+ name = paxPath;
98
+ paxPath = null;
99
+ }
100
+
101
+ // Only process regular files
102
+ if (typeFlag !== 48 && typeFlag !== 0) continue;
103
+ if (size === 0) continue;
104
+
105
+ // Match CRD YAML files — source tarball has them at config/crds/resources/
106
+ if (name.includes("config/crds/resources/") && name.endsWith(".yaml")) {
107
+ const basename = name.split("/").pop() || name;
108
+ crds.set(basename, Buffer.from(fileData));
109
+ }
110
+ }
111
+
112
+ return crds;
113
+ }
114
+
115
+ export function getCachePath(version: string = KCC_VERSION): string {
116
+ return cacheFile(version);
117
+ }
118
+
119
+ export function clearCache(version: string = KCC_VERSION): void {
120
+ clearCacheFile(cacheFile(version));
121
+ }
@@ -0,0 +1,163 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { readFileSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import {
6
+ parseGcpCRD,
7
+ gcpServiceName,
8
+ stripServicePrefix,
9
+ gcpTypeName,
10
+ gcpShortName,
11
+ } from "./parse";
12
+
13
+ const __dirname_ = dirname(fileURLToPath(import.meta.url));
14
+ const testdata = join(__dirname_, "..", "testdata");
15
+
16
+ describe("gcpServiceName", () => {
17
+ test("extracts service from CNRM group", () => {
18
+ expect(gcpServiceName("compute.cnrm.cloud.google.com")).toBe("Compute");
19
+ expect(gcpServiceName("storage.cnrm.cloud.google.com")).toBe("Storage");
20
+ expect(gcpServiceName("iam.cnrm.cloud.google.com")).toBe("Iam");
21
+ expect(gcpServiceName("container.cnrm.cloud.google.com")).toBe("Container");
22
+ expect(gcpServiceName("sql.cnrm.cloud.google.com")).toBe("Sql");
23
+ });
24
+ });
25
+
26
+ describe("stripServicePrefix", () => {
27
+ test("strips known prefix", () => {
28
+ expect(stripServicePrefix("ComputeInstance", "Compute")).toBe("Instance");
29
+ expect(stripServicePrefix("StorageBucket", "Storage")).toBe("Bucket");
30
+ expect(stripServicePrefix("IAMPolicyMember", "IAM")).toBe("PolicyMember");
31
+ });
32
+
33
+ test("returns kind if no prefix match", () => {
34
+ expect(stripServicePrefix("VPCNetwork", "Compute")).toBe("VPCNetwork");
35
+ });
36
+ });
37
+
38
+ describe("gcpTypeName", () => {
39
+ test("builds full type name", () => {
40
+ expect(gcpTypeName("compute.cnrm.cloud.google.com", "ComputeInstance")).toBe("GCP::Compute::Instance");
41
+ expect(gcpTypeName("storage.cnrm.cloud.google.com", "StorageBucket")).toBe("GCP::Storage::Bucket");
42
+ });
43
+ });
44
+
45
+ describe("gcpShortName", () => {
46
+ test("extracts short name", () => {
47
+ expect(gcpShortName("GCP::Compute::Instance")).toBe("Instance");
48
+ expect(gcpShortName("GCP::Storage::Bucket")).toBe("Bucket");
49
+ });
50
+ });
51
+
52
+ describe("parseGcpCRD", () => {
53
+ test("parses ComputeInstance CRD", () => {
54
+ const content = readFileSync(join(testdata, "compute-instance.yaml"), "utf-8");
55
+ const results = parseGcpCRD(content);
56
+
57
+ expect(results).toHaveLength(1);
58
+ const result = results[0];
59
+
60
+ expect(result.resource.typeName).toBe("GCP::Compute::Instance");
61
+ expect(result.gvk.group).toBe("compute.cnrm.cloud.google.com");
62
+ expect(result.gvk.version).toBe("v1beta1");
63
+ expect(result.gvk.kind).toBe("ComputeInstance");
64
+
65
+ // Check properties
66
+ const propNames = result.resource.properties.map((p) => p.name);
67
+ expect(propNames).toContain("machineType");
68
+ expect(propNames).toContain("zone");
69
+ expect(propNames).toContain("canIpForward");
70
+ expect(propNames).toContain("tags");
71
+
72
+ // Check required
73
+ const machineType = result.resource.properties.find((p) => p.name === "machineType")!;
74
+ expect(machineType.required).toBe(true);
75
+ expect(machineType.tsType).toBe("string");
76
+
77
+ // Check resourceRef detection
78
+ const netRef = result.resource.properties.find((p) => p.name === "networkInterfaceRef");
79
+ expect(netRef).toBeDefined();
80
+ expect(netRef!.isResourceRef).toBe(true);
81
+
82
+ // Check attributes
83
+ expect(result.resource.attributes).toHaveLength(3);
84
+ expect(result.resource.attributes[0].name).toBe("name");
85
+ });
86
+
87
+ test("parses StorageBucket CRD", () => {
88
+ const content = readFileSync(join(testdata, "storage-bucket.yaml"), "utf-8");
89
+ const results = parseGcpCRD(content);
90
+
91
+ expect(results).toHaveLength(1);
92
+ const result = results[0];
93
+
94
+ expect(result.resource.typeName).toBe("GCP::Storage::Bucket");
95
+
96
+ // Check enum
97
+ const storageClass = result.resource.properties.find((p) => p.name === "storageClass");
98
+ expect(storageClass).toBeDefined();
99
+ expect(storageClass!.enum).toContain("STANDARD");
100
+ expect(storageClass!.enum).toContain("NEARLINE");
101
+
102
+ // Check property types (nested objects)
103
+ expect(result.propertyTypes.length).toBeGreaterThan(0);
104
+ const versioning = result.propertyTypes.find((pt) => pt.specType === "versioning");
105
+ expect(versioning).toBeDefined();
106
+ });
107
+
108
+ test("parses IAMPolicyMember CRD", () => {
109
+ const content = readFileSync(join(testdata, "iam-policy-member.yaml"), "utf-8");
110
+ const results = parseGcpCRD(content);
111
+
112
+ expect(results).toHaveLength(1);
113
+ const result = results[0];
114
+
115
+ expect(result.resource.typeName).toBe("GCP::Iam::PolicyMember");
116
+
117
+ // Check required properties
118
+ const member = result.resource.properties.find((p) => p.name === "member")!;
119
+ expect(member.required).toBe(true);
120
+
121
+ const role = result.resource.properties.find((p) => p.name === "role")!;
122
+ expect(role.required).toBe(true);
123
+
124
+ // resourceRef should be detected
125
+ const resRef = result.resource.properties.find((p) => p.name === "resourceRef");
126
+ expect(resRef).toBeDefined();
127
+ expect(resRef!.isResourceRef).toBe(true);
128
+ });
129
+
130
+ test("handles multi-document CRD bundle", () => {
131
+ const content = [
132
+ readFileSync(join(testdata, "compute-instance.yaml"), "utf-8"),
133
+ readFileSync(join(testdata, "storage-bucket.yaml"), "utf-8"),
134
+ ].join("\n---\n");
135
+
136
+ const results = parseGcpCRD(content);
137
+ expect(results).toHaveLength(2);
138
+ expect(results.map((r) => r.resource.typeName)).toContain("GCP::Compute::Instance");
139
+ expect(results.map((r) => r.resource.typeName)).toContain("GCP::Storage::Bucket");
140
+ });
141
+
142
+ test("skips non-CNRM CRDs", () => {
143
+ const content = `
144
+ apiVersion: apiextensions.k8s.io/v1
145
+ kind: CustomResourceDefinition
146
+ metadata:
147
+ name: foo.example.com
148
+ spec:
149
+ group: example.com
150
+ names:
151
+ kind: Foo
152
+ versions:
153
+ - name: v1
154
+ served: true
155
+ storage: true
156
+ schema:
157
+ openAPIV3Schema:
158
+ type: object
159
+ `;
160
+ const results = parseGcpCRD(content);
161
+ expect(results).toHaveLength(0);
162
+ });
163
+ });