@intentius/chant-lexicon-gcp 0.124.31 → 0.124.32

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 (103) hide show
  1. package/dist/actions/index.js +45 -0
  2. package/dist/api/read-client.js +105 -0
  3. package/dist/codegen/docs-cli.js +6 -0
  4. package/dist/codegen/docs.js +115 -0
  5. package/dist/codegen/generate-cli.js +17 -0
  6. package/dist/codegen/generate.js +214 -0
  7. package/dist/codegen/naming.js +164 -0
  8. package/dist/codegen/package.js +43 -0
  9. package/dist/composites/catalog.js +1245 -0
  10. package/dist/composites/cloud-function.js +65 -0
  11. package/dist/composites/cloud-run-service.js +71 -0
  12. package/dist/composites/cloud-sql-instance.js +85 -0
  13. package/dist/composites/gcs-bucket.js +64 -0
  14. package/dist/composites/gke-cluster.js +113 -0
  15. package/dist/composites/gke-crdb-region.js +137 -0
  16. package/dist/composites/gpu-node-pool.js +74 -0
  17. package/dist/composites/index.js +15 -0
  18. package/dist/composites/landing-zone.js +89 -0
  19. package/dist/composites/managed-certificate.js +46 -0
  20. package/dist/composites/memorystore-redis.js +52 -0
  21. package/dist/composites/multi-region-vpc.js +108 -0
  22. package/dist/composites/private-service.js +50 -0
  23. package/dist/composites/pubsub-pipeline.js +63 -0
  24. package/dist/composites/secure-project.js +85 -0
  25. package/dist/composites/vpc-network.js +113 -0
  26. package/dist/coverage.js +28 -0
  27. package/dist/deep-observe-hooks.js +152 -0
  28. package/dist/deep-observe.js +273 -0
  29. package/dist/default-labels.js +67 -0
  30. package/dist/describe-resources.js +264 -0
  31. package/dist/detect.js +16 -0
  32. package/dist/export-resources.js +59 -0
  33. package/dist/generated/index.js +1804 -0
  34. package/dist/generated/runtime.js +4 -0
  35. package/dist/governance.js +98 -0
  36. package/dist/import/generator.js +68 -0
  37. package/dist/import/live-export.js +73 -0
  38. package/dist/import/parser.js +61 -0
  39. package/dist/index.js +36 -0
  40. package/dist/integrity.json +3 -3
  41. package/dist/lint/audit-catalog.js +38 -0
  42. package/dist/lint/audit-lineage.js +63 -0
  43. package/dist/lint/post-synth/gcp-helpers.js +91 -0
  44. package/dist/lint/post-synth/index.js +54 -0
  45. package/dist/lint/post-synth/schema-registry.d.ts.map +1 -1
  46. package/dist/lint/post-synth/schema-registry.js +69 -0
  47. package/dist/lint/post-synth/wgc101.js +47 -0
  48. package/dist/lint/post-synth/wgc102.js +29 -0
  49. package/dist/lint/post-synth/wgc103.js +37 -0
  50. package/dist/lint/post-synth/wgc104.js +34 -0
  51. package/dist/lint/post-synth/wgc105.js +39 -0
  52. package/dist/lint/post-synth/wgc106.js +30 -0
  53. package/dist/lint/post-synth/wgc107.js +33 -0
  54. package/dist/lint/post-synth/wgc108.js +34 -0
  55. package/dist/lint/post-synth/wgc109.js +33 -0
  56. package/dist/lint/post-synth/wgc110.js +32 -0
  57. package/dist/lint/post-synth/wgc111.js +47 -0
  58. package/dist/lint/post-synth/wgc112.js +46 -0
  59. package/dist/lint/post-synth/wgc113.js +35 -0
  60. package/dist/lint/post-synth/wgc201.js +30 -0
  61. package/dist/lint/post-synth/wgc202.js +33 -0
  62. package/dist/lint/post-synth/wgc203.js +35 -0
  63. package/dist/lint/post-synth/wgc204.js +33 -0
  64. package/dist/lint/post-synth/wgc301.js +28 -0
  65. package/dist/lint/post-synth/wgc302.js +28 -0
  66. package/dist/lint/post-synth/wgc303.js +29 -0
  67. package/dist/lint/post-synth/wgc401.js +50 -0
  68. package/dist/lint/post-synth/wgc402.js +46 -0
  69. package/dist/lint/post-synth/wgc403.js +74 -0
  70. package/dist/lint/post-synth/wgc501.js +63 -0
  71. package/dist/lint/post-synth/wgc502.js +37 -0
  72. package/dist/lint/post-synth/wgc503.js +49 -0
  73. package/dist/lint/rules/hardcoded-project.js +51 -0
  74. package/dist/lint/rules/hardcoded-region.js +51 -0
  75. package/dist/lint/rules/index.js +3 -0
  76. package/dist/lint/rules/public-iam.js +37 -0
  77. package/dist/lsp/completions.d.ts.map +1 -1
  78. package/dist/lsp/completions.js +15 -0
  79. package/dist/lsp/hover.d.ts.map +1 -1
  80. package/dist/lsp/hover.js +46 -0
  81. package/dist/manifest.json +1 -1
  82. package/dist/op/activities/floci-gcp.js +49 -0
  83. package/dist/op/activities/gcp-apply.js +618 -0
  84. package/dist/op/activities/index.js +10 -0
  85. package/dist/op/builders.js +50 -0
  86. package/dist/ownership.js +38 -0
  87. package/dist/package-cli.js +20 -0
  88. package/dist/plugin.js +371 -0
  89. package/dist/pseudo.js +15 -0
  90. package/dist/rules/schema-registry.ts +2 -1
  91. package/dist/serializer.d.ts.map +1 -1
  92. package/dist/serializer.js +233 -0
  93. package/dist/spec/fetch.js +107 -0
  94. package/dist/spec/parse.js +323 -0
  95. package/dist/validate-cli.js +11 -0
  96. package/dist/validate.js +24 -0
  97. package/dist/variables.js +15 -0
  98. package/package.json +8 -8
  99. package/src/codegen/package.ts +2 -2
  100. package/src/lint/post-synth/schema-registry.ts +2 -1
  101. package/src/lsp/completions.ts +2 -1
  102. package/src/lsp/hover.ts +2 -1
  103. package/src/serializer.ts +2 -1
@@ -0,0 +1,233 @@
1
+ /**
2
+ * GCP Config Connector YAML serializer.
3
+ *
4
+ * Converts Chant declarables to multi-document K8s YAML output
5
+ * for Config Connector resources with apiVersion, kind, metadata, and spec.
6
+ */
7
+ import { createRequire } from "module";
8
+ import { moduleSourceUrl } from "@intentius/chant/module-source";
9
+ import { isPropertyDeclarable, isResourceDeclarable } from "@intentius/chant/declarable";
10
+ import { ownershipEntries, LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
11
+ import { walkValue } from "@intentius/chant/serializer-walker";
12
+ import { emitYAML } from "@intentius/chant/yaml";
13
+ import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
14
+ import { isDefaultLabels, isDefaultAnnotations } from "./default-labels.js";
15
+ const require = createRequire(moduleSourceUrl(import.meta.url));
16
+ let cachedGVKMap = null;
17
+ function getGVKMap() {
18
+ if (cachedGVKMap)
19
+ return cachedGVKMap;
20
+ try {
21
+ cachedGVKMap = require("./generated/lexicon-gcp.json");
22
+ }
23
+ catch {
24
+ cachedGVKMap = {};
25
+ }
26
+ return cachedGVKMap;
27
+ }
28
+ /**
29
+ * Resolve entityType to apiVersion and kind.
30
+ *
31
+ * Exported for the read paths (./describe-resources.ts): the CNRM kind is the
32
+ * key into the applier's mapper table, and the packaged map is the only place
33
+ * that knows `GCP::Pubsub::Topic` is `PubSubTopic` rather than the naive
34
+ * `PubsubTopic` the fallback derivation would produce.
35
+ */
36
+ export function resolveGVK(entityType) {
37
+ const gvkMap = getGVKMap();
38
+ for (const entry of Object.values(gvkMap)) {
39
+ if (entry.resourceType === entityType && entry.apiVersion && entry.gvkKind) {
40
+ return { apiVersion: entry.apiVersion, kind: entry.gvkKind };
41
+ }
42
+ }
43
+ // Fallback: derive from entity type string (GCP::Service::Kind → service.cnrm.cloud.google.com/v1beta1, Kind)
44
+ return deriveGVKFromType(entityType);
45
+ }
46
+ /**
47
+ * Derive GVK from a GCP type name.
48
+ * "GCP::Compute::Instance" → { apiVersion: "compute.cnrm.cloud.google.com/v1beta1", kind: "ComputeInstance" }
49
+ */
50
+ function deriveGVKFromType(entityType) {
51
+ const parts = entityType.split("::");
52
+ if (parts.length !== 3 || parts[0] !== "GCP")
53
+ return null;
54
+ const service = parts[1].toLowerCase();
55
+ const shortKind = parts[2];
56
+ const group = `${service}.cnrm.cloud.google.com`;
57
+ const apiVersion = `${group}/v1beta1`;
58
+ const kind = `${parts[1]}${shortKind}`;
59
+ return { apiVersion, kind };
60
+ }
61
+ /**
62
+ * GCP visitor for the generic serializer walker.
63
+ */
64
+ function gcpVisitor(entityNames) {
65
+ return {
66
+ attrRef: (name, _attr) => {
67
+ // For Config Connector, references resolve to metadata.name
68
+ return name;
69
+ },
70
+ resourceRef: (name) => {
71
+ // Config Connector uses resourceRef pattern: { name: "resource-name" }
72
+ return { name };
73
+ },
74
+ propertyDeclarable: (entity, walk) => {
75
+ if (!isResourceDeclarable(entity) || typeof entity.props !== "object" || entity.props === null) {
76
+ return undefined;
77
+ }
78
+ const props = entity.props;
79
+ const result = {};
80
+ for (const [key, value] of Object.entries(props)) {
81
+ if (value !== undefined) {
82
+ result[key] = walk(value);
83
+ }
84
+ }
85
+ return Object.keys(result).length > 0 ? result : undefined;
86
+ },
87
+ };
88
+ }
89
+ /**
90
+ * Convert a value to YAML-compatible form using the walker.
91
+ */
92
+ function toYAMLValue(value, entityNames) {
93
+ return walkValue(value, entityNames, gcpVisitor(entityNames));
94
+ }
95
+ /**
96
+ * GCP Config Connector YAML serializer implementation.
97
+ */
98
+ export const gcpSerializer = {
99
+ name: "gcp",
100
+ rulePrefix: "WGC",
101
+ serialize(entities, _outputs, context) {
102
+ // Build reverse map: entity → name
103
+ const entityNames = new Map();
104
+ for (const [name, entity] of entities) {
105
+ entityNames.set(entity, name);
106
+ }
107
+ // Collect default labels and annotations. Ownership markers are stamped as
108
+ // labels (Config Connector resources carry them in metadata.labels).
109
+ let defaultLabelEntries = context?.ownership
110
+ ? { ...ownershipEntries(LABEL_OWNERSHIP_KEYS, context.ownership) }
111
+ : {};
112
+ let defaultAnnotationEntries = {};
113
+ for (const [, entity] of entities) {
114
+ if (isDefaultLabels(entity)) {
115
+ defaultLabelEntries = { ...defaultLabelEntries, ...entity.labels };
116
+ }
117
+ if (isDefaultAnnotations(entity)) {
118
+ defaultAnnotationEntries = { ...defaultAnnotationEntries, ...entity.annotations };
119
+ }
120
+ }
121
+ const documents = [];
122
+ for (const [name, entity] of entities) {
123
+ if (isPropertyDeclarable(entity))
124
+ continue;
125
+ if (isDefaultLabels(entity) || isDefaultAnnotations(entity))
126
+ continue;
127
+ const entityType = entity.entityType;
128
+ const gvk = resolveGVK(entityType);
129
+ if (!gvk)
130
+ continue;
131
+ const props = toYAMLValue(isResourceDeclarable(entity) ? entity.props : undefined, entityNames);
132
+ if (!props)
133
+ continue;
134
+ // Build the Config Connector manifest structure
135
+ const manifest = {
136
+ apiVersion: gvk.apiVersion,
137
+ kind: gvk.kind,
138
+ };
139
+ // Build metadata
140
+ const metadata = props.metadata ?? {};
141
+ if (!metadata.name) {
142
+ metadata.name = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
143
+ }
144
+ // Merge default labels
145
+ if (Object.keys(defaultLabelEntries).length > 0) {
146
+ const existingLabels = (metadata.labels ?? {});
147
+ metadata.labels = { ...defaultLabelEntries, ...existingLabels };
148
+ }
149
+ // Merge default annotations (resolve PseudoParameters to env-var strings)
150
+ if (Object.keys(defaultAnnotationEntries).length > 0) {
151
+ const resolvedAnnotations = {};
152
+ for (const [k, v] of Object.entries(defaultAnnotationEntries)) {
153
+ resolvedAnnotations[k] = resolveAnnotationValue(v);
154
+ }
155
+ const existingAnnotations = (metadata.annotations ?? {});
156
+ metadata.annotations = { ...resolvedAnnotations, ...existingAnnotations };
157
+ }
158
+ manifest.metadata = metadata;
159
+ // All Config Connector resources use spec
160
+ const spec = {};
161
+ for (const [key, value] of Object.entries(props)) {
162
+ if (key !== "metadata") {
163
+ spec[key] = value;
164
+ }
165
+ }
166
+ if (Object.keys(spec).length > 0) {
167
+ manifest.spec = spec;
168
+ }
169
+ const yamlDoc = emitK8sManifest(manifest);
170
+ documents.push(yamlDoc);
171
+ }
172
+ return documents.join("\n---\n");
173
+ },
174
+ };
175
+ /**
176
+ * Pseudo-parameter → environment variable mapping.
177
+ * Config Connector annotations must be plain strings, so PseudoParameters
178
+ * are resolved from environment variables at build time.
179
+ */
180
+ const PSEUDO_ENV_MAP = {
181
+ "GCP::ProjectId": { envVar: "GCP_PROJECT_ID", fallback: "PROJECT_ID" },
182
+ "GCP::Region": { envVar: "GCP_REGION", fallback: "us-central1" },
183
+ "GCP::Zone": { envVar: "GCP_ZONE", fallback: "us-central1-a" },
184
+ };
185
+ /**
186
+ * Resolve an annotation value to a plain string.
187
+ * PseudoParameter intrinsics are resolved from environment variables;
188
+ * other values pass through unchanged.
189
+ */
190
+ function resolveAnnotationValue(value) {
191
+ if (typeof value === "object" && value !== null && INTRINSIC_MARKER in value) {
192
+ if ("toJSON" in value && typeof value.toJSON === "function") {
193
+ const json = value.toJSON();
194
+ if (json && typeof json === "object" && "Ref" in json && typeof json.Ref === "string") {
195
+ const mapping = PSEUDO_ENV_MAP[json.Ref];
196
+ if (mapping) {
197
+ return process.env[mapping.envVar] ?? mapping.fallback;
198
+ }
199
+ }
200
+ }
201
+ return String(value);
202
+ }
203
+ return value;
204
+ }
205
+ /**
206
+ * Emit a key-value pair as YAML.
207
+ */
208
+ function emitKeyValue(key, value) {
209
+ const yamlStr = emitYAML(value, 1);
210
+ if (yamlStr.startsWith("\n")) {
211
+ return `${key}:${yamlStr}`;
212
+ }
213
+ return `${key}: ${yamlStr}`;
214
+ }
215
+ /**
216
+ * Emit a Config Connector manifest object as YAML.
217
+ * Preserves key ordering: apiVersion, kind, metadata, spec.
218
+ */
219
+ function emitK8sManifest(manifest) {
220
+ const orderedKeys = ["apiVersion", "kind", "metadata", "spec"];
221
+ const lines = [];
222
+ for (const key of orderedKeys) {
223
+ if (manifest[key] !== undefined) {
224
+ lines.push(emitKeyValue(key, manifest[key]));
225
+ }
226
+ }
227
+ for (const [key, value] of Object.entries(manifest)) {
228
+ if (!orderedKeys.includes(key) && value !== undefined) {
229
+ lines.push(emitKeyValue(key, value));
230
+ }
231
+ }
232
+ return lines.join("\n") + "\n";
233
+ }
@@ -0,0 +1,107 @@
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
+ import { homedir } from "os";
7
+ import { join } from "path";
8
+ import { fetchWithCache, clearCacheFile } from "@intentius/chant/codegen/fetch";
9
+ /** Per-attempt download timeout for a multi-megabyte archive. */
10
+ const ARCHIVE_ATTEMPT_TIMEOUT_MS = 120000;
11
+ /** A tagged archive never changes, so a cached copy stays good for a week. */
12
+ const ARCHIVE_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
13
+ /**
14
+ * Pinned Config Connector version for reproducible codegen.
15
+ */
16
+ export const KCC_VERSION = "v1.152.0";
17
+ const CACHE_DIR = join(homedir(), ".chant");
18
+ function bundleUrl(version) {
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
+ function cacheFile(version) {
24
+ return join(CACHE_DIR, `kcc-crds-${version}.tar.gz`);
25
+ }
26
+ /**
27
+ * Fetch the Config Connector CRD bundle and extract CRD YAML files.
28
+ *
29
+ * Returns a Map keyed by CRD filename (e.g., "computeinstance.yaml") with
30
+ * the raw YAML content as a Buffer.
31
+ */
32
+ export async function fetchCRDBundle(force = false, version = KCC_VERSION) {
33
+ const url = bundleUrl(version);
34
+ const cache = cacheFile(version);
35
+ // The archive is a whole source tree at a pinned tag: tens of megabytes,
36
+ // immutable, so it gets a long attempt and a week of cache.
37
+ const tarData = await fetchWithCache({ url, cacheFile: cache, attemptTimeoutMs: ARCHIVE_ATTEMPT_TIMEOUT_MS, cacheTtlMs: ARCHIVE_CACHE_TTL_MS }, force);
38
+ return extractCRDs(tarData);
39
+ }
40
+ /**
41
+ * Extract CRD YAML files from a tar.gz bundle.
42
+ *
43
+ * Filters for entries under a crds/ directory or standalone CRD YAML files.
44
+ */
45
+ async function extractCRDs(tarData) {
46
+ const { gunzipSync } = await import("fflate");
47
+ const crds = new Map();
48
+ // Decompress gzip synchronously (avoids lingering event loop references)
49
+ const gunzipped = gunzipSync(new Uint8Array(tarData));
50
+ // Parse tar (512-byte header blocks) with PAX extended header support.
51
+ // GitHub source tarballs use PAX headers (type 'x') for long filenames.
52
+ let offset = 0;
53
+ const data = gunzipped;
54
+ let paxPath = null;
55
+ while (offset < data.length - 512) {
56
+ const header = data.slice(offset, offset + 512);
57
+ offset += 512;
58
+ if (header.every((b) => b === 0))
59
+ break;
60
+ // Extract filename: ustar prefix (bytes 345-499) + name (bytes 0-99)
61
+ const nameBytes = header.slice(0, 100);
62
+ const nameEnd = nameBytes.indexOf(0);
63
+ let name = new TextDecoder().decode(nameBytes.slice(0, nameEnd > 0 ? nameEnd : 100)).trim();
64
+ const prefixBytes = header.slice(345, 500);
65
+ const prefixEnd = prefixBytes.indexOf(0);
66
+ const ustarPrefix = new TextDecoder().decode(prefixBytes.slice(0, prefixEnd > 0 ? prefixEnd : 155)).trim();
67
+ if (ustarPrefix)
68
+ name = `${ustarPrefix}/${name}`;
69
+ const sizeStr = new TextDecoder().decode(header.slice(124, 136)).trim().replace(/\0/g, "");
70
+ const size = parseInt(sizeStr, 8) || 0;
71
+ const typeFlag = header[156];
72
+ const typeFlagChar = String.fromCharCode(typeFlag);
73
+ const blocks = Math.ceil(size / 512);
74
+ const fileData = data.slice(offset, offset + size);
75
+ offset += blocks * 512;
76
+ // PAX extended header — extract path= for the next entry
77
+ if (typeFlagChar === "x") {
78
+ const paxStr = new TextDecoder().decode(fileData);
79
+ const match = paxStr.match(/\bpath=([^\n]+)/);
80
+ if (match)
81
+ paxPath = match[1];
82
+ continue;
83
+ }
84
+ // Use PAX path if available, then reset
85
+ if (paxPath) {
86
+ name = paxPath;
87
+ paxPath = null;
88
+ }
89
+ // Only process regular files
90
+ if (typeFlag !== 48 && typeFlag !== 0)
91
+ continue;
92
+ if (size === 0)
93
+ continue;
94
+ // Match CRD YAML files — source tarball has them at config/crds/resources/
95
+ if (name.includes("config/crds/resources/") && name.endsWith(".yaml")) {
96
+ const basename = name.split("/").pop() || name;
97
+ crds.set(basename, Buffer.from(fileData));
98
+ }
99
+ }
100
+ return crds;
101
+ }
102
+ export function getCachePath(version = KCC_VERSION) {
103
+ return cacheFile(version);
104
+ }
105
+ export function clearCache(version = KCC_VERSION) {
106
+ clearCacheFile(cacheFile(version));
107
+ }
@@ -0,0 +1,323 @@
1
+ /**
2
+ * CRD parser for GCP Config Connector resources.
3
+ *
4
+ * Adapts the K8s CRD parser pattern for GCP-specific concerns:
5
+ * - Type naming: GCP::{Service}::{Kind}
6
+ * - resourceRef detection
7
+ * - Status field skipping
8
+ */
9
+ import yaml from "js-yaml";
10
+ // ── Naming helpers ──────────────────────────────────────────────────
11
+ /**
12
+ * Extract GCP service name from a CNRM group.
13
+ * "compute.cnrm.cloud.google.com" → "Compute"
14
+ */
15
+ export function gcpServiceName(group) {
16
+ const firstSegment = group.split(".")[0];
17
+ return firstSegment.charAt(0).toUpperCase() + firstSegment.slice(1);
18
+ }
19
+ /**
20
+ * Strip the service prefix from a CRD kind.
21
+ * "ComputeInstance", "Compute" → "Instance"
22
+ * "IAMPolicyMember", "Iam" → "PolicyMember"
23
+ */
24
+ export function stripServicePrefix(kind, service) {
25
+ // Direct match (e.g., "Compute" prefix in "ComputeInstance")
26
+ if (kind.startsWith(service)) {
27
+ const rest = kind.slice(service.length);
28
+ if (rest.length > 0 && rest[0] === rest[0].toUpperCase()) {
29
+ return rest;
30
+ }
31
+ }
32
+ // Case-insensitive match (e.g., "IAM" prefix when service is "Iam")
33
+ const kindLower = kind.toLowerCase();
34
+ const serviceLower = service.toLowerCase();
35
+ if (kindLower.startsWith(serviceLower)) {
36
+ const rest = kind.slice(service.length);
37
+ if (rest.length > 0 && rest[0] === rest[0].toUpperCase()) {
38
+ return rest;
39
+ }
40
+ }
41
+ return kind;
42
+ }
43
+ /**
44
+ * Build full GCP type name.
45
+ * "compute.cnrm.cloud.google.com", "ComputeInstance" → "GCP::Compute::Instance"
46
+ */
47
+ export function gcpTypeName(group, kind) {
48
+ const service = gcpServiceName(group);
49
+ const shortKind = stripServicePrefix(kind, service);
50
+ return `GCP::${service}::${shortKind}`;
51
+ }
52
+ /**
53
+ * Extract short name from a GCP type.
54
+ * "GCP::Compute::Instance" → "Instance"
55
+ */
56
+ export function gcpShortName(typeName) {
57
+ const parts = typeName.split("::");
58
+ return parts[parts.length - 1];
59
+ }
60
+ // ── CRD Parsing ─────────────────────────────────────────────────────
61
+ /**
62
+ * Parse a CRD YAML document string into GcpParseResult entries.
63
+ * Supports multi-document YAML for CRD bundles.
64
+ */
65
+ export function parseGcpCRD(content) {
66
+ const text = typeof content === "string" ? content : content.toString("utf-8");
67
+ const results = [];
68
+ const documents = text
69
+ .split(/^---\s*$/m)
70
+ .map((d) => d.trim())
71
+ .filter((d) => d.length > 0);
72
+ for (const docStr of documents) {
73
+ const doc = yaml.load(docStr);
74
+ if (!doc || doc.kind !== "CustomResourceDefinition")
75
+ continue;
76
+ const spec = doc.spec;
77
+ if (!spec?.group || !spec?.names?.kind || !spec?.versions)
78
+ continue;
79
+ // Only process groups that are Config Connector CRDs
80
+ if (!spec.group.includes("cnrm.cloud.google.com"))
81
+ continue;
82
+ const crdResults = parseCRDSpec(spec);
83
+ results.push(...crdResults);
84
+ }
85
+ return results;
86
+ }
87
+ /**
88
+ * Parse a CRD spec into GcpParseResult entries.
89
+ */
90
+ function parseCRDSpec(spec) {
91
+ const results = [];
92
+ // Find the storage version (canonical)
93
+ const storageVersion = spec.versions.find((v) => v.storage && v.served);
94
+ const targetVersion = storageVersion ?? spec.versions.find((v) => v.served);
95
+ if (!targetVersion)
96
+ return results;
97
+ const typeName = gcpTypeName(spec.group, spec.names.kind);
98
+ const gvk = {
99
+ group: spec.group,
100
+ version: targetVersion.name,
101
+ kind: spec.names.kind,
102
+ };
103
+ const schema = targetVersion.schema?.openAPIV3Schema;
104
+ const specSchema = schema?.properties?.spec;
105
+ const properties = specSchema ? extractSpecProperties(specSchema) : [];
106
+ const propertyTypes = specSchema ? extractPropertyTypes(specSchema, typeName) : [];
107
+ const enums = specSchema ? extractEnums(specSchema, typeName) : [];
108
+ results.push({
109
+ resource: {
110
+ typeName,
111
+ description: `Config Connector resource: ${spec.names.kind} (${spec.group})`,
112
+ properties,
113
+ attributes: [
114
+ { name: "name", tsType: "string" },
115
+ { name: "namespace", tsType: "string" },
116
+ { name: "uid", tsType: "string" },
117
+ ],
118
+ deprecatedProperties: [],
119
+ },
120
+ propertyTypes,
121
+ enums,
122
+ gvk,
123
+ });
124
+ return results;
125
+ }
126
+ // ── Property extraction ─────────────────────────────────────────────
127
+ /**
128
+ * Extract properties from the spec sub-object of a CRD schema.
129
+ */
130
+ function extractSpecProperties(specSchema) {
131
+ const result = [];
132
+ const props = specSchema.properties ?? {};
133
+ const requiredSet = new Set(specSchema.required ?? []);
134
+ for (const [name, prop] of Object.entries(props)) {
135
+ const isRef = isResourceRef(prop);
136
+ result.push({
137
+ name,
138
+ tsType: resolveSchemaType(prop),
139
+ required: requiredSet.has(name),
140
+ description: prop.description,
141
+ enum: prop.enum,
142
+ constraints: extractConstraints(prop),
143
+ ...(isRef && { isResourceRef: true }),
144
+ });
145
+ }
146
+ return result;
147
+ }
148
+ /**
149
+ * Detect Config Connector resourceRef fields.
150
+ * These typically have `name` and `external` sub-properties,
151
+ * optionally with `namespace`, `kind`, and `apiVersion`.
152
+ */
153
+ function isResourceRef(schema) {
154
+ if (schema.type !== "object" || !schema.properties)
155
+ return false;
156
+ const keys = new Set(Object.keys(schema.properties));
157
+ // Must have either `name` or `external`
158
+ if (!keys.has("name") && !keys.has("external"))
159
+ return false;
160
+ // Must be a small object with only ref-like keys
161
+ const refKeys = new Set(["name", "external", "namespace", "kind", "apiVersion"]);
162
+ for (const key of keys) {
163
+ if (!refKeys.has(key))
164
+ return false;
165
+ }
166
+ return keys.size >= 2;
167
+ }
168
+ /**
169
+ * Extract nested object types as ParsedPropertyType entries.
170
+ */
171
+ function extractPropertyTypes(specSchema, parentTypeName) {
172
+ const results = [];
173
+ const props = specSchema.properties ?? {};
174
+ for (const [name, prop] of Object.entries(props)) {
175
+ // Skip resourceRef fields — they are treated as references, not nested types
176
+ if (isResourceRef(prop))
177
+ continue;
178
+ // Inline object definitions
179
+ if (prop.type === "object" && prop.properties) {
180
+ const ptName = `${parentTypeName}::${pascalCase(name)}`;
181
+ const requiredSet = new Set(prop.required ?? []);
182
+ results.push({
183
+ name: ptName,
184
+ specType: name,
185
+ properties: Object.entries(prop.properties).map(([pName, pSchema]) => ({
186
+ name: pName,
187
+ tsType: resolveSchemaType(pSchema),
188
+ required: requiredSet.has(pName),
189
+ description: pSchema.description,
190
+ enum: pSchema.enum,
191
+ constraints: extractConstraints(pSchema),
192
+ ...(isResourceRef(pSchema) && { isResourceRef: true }),
193
+ })),
194
+ });
195
+ // Recurse into nested objects
196
+ for (const [subName, subProp] of Object.entries(prop.properties)) {
197
+ if (subProp.type === "object" && subProp.properties && !isResourceRef(subProp)) {
198
+ const subPtName = `${ptName}::${pascalCase(subName)}`;
199
+ const subRequiredSet = new Set(subProp.required ?? []);
200
+ results.push({
201
+ name: subPtName,
202
+ specType: subName,
203
+ properties: Object.entries(subProp.properties).map(([spName, spSchema]) => ({
204
+ name: spName,
205
+ tsType: resolveSchemaType(spSchema),
206
+ required: subRequiredSet.has(spName),
207
+ description: spSchema.description,
208
+ enum: spSchema.enum,
209
+ constraints: extractConstraints(spSchema),
210
+ })),
211
+ });
212
+ }
213
+ }
214
+ }
215
+ // Array of objects
216
+ if (prop.type === "array" && prop.items?.type === "object" && prop.items.properties) {
217
+ if (isResourceRef(prop.items))
218
+ continue;
219
+ const itemSchema = prop.items;
220
+ const ptName = `${parentTypeName}::${pascalCase(singularize(name))}`;
221
+ const requiredSet = new Set(itemSchema.required ?? []);
222
+ results.push({
223
+ name: ptName,
224
+ specType: name,
225
+ properties: Object.entries(itemSchema.properties).map(([pName, pSchema]) => ({
226
+ name: pName,
227
+ tsType: resolveSchemaType(pSchema),
228
+ required: requiredSet.has(pName),
229
+ description: pSchema.description,
230
+ enum: pSchema.enum,
231
+ constraints: extractConstraints(pSchema),
232
+ ...(isResourceRef(pSchema) && { isResourceRef: true }),
233
+ })),
234
+ });
235
+ }
236
+ }
237
+ return results;
238
+ }
239
+ /**
240
+ * Extract enum types from the spec schema.
241
+ */
242
+ function extractEnums(specSchema, parentTypeName) {
243
+ const results = [];
244
+ const props = specSchema.properties ?? {};
245
+ for (const [name, prop] of Object.entries(props)) {
246
+ if (prop.enum && prop.enum.length > 0) {
247
+ results.push({
248
+ name: `${parentTypeName}::${pascalCase(name)}`,
249
+ values: prop.enum,
250
+ });
251
+ }
252
+ }
253
+ return results;
254
+ }
255
+ // ── Schema type resolution ──────────────────────────────────────────
256
+ function resolveSchemaType(schema) {
257
+ if (!schema)
258
+ return "any";
259
+ if (schema["x-kubernetes-int-or-string"])
260
+ return "string | number";
261
+ if (schema["x-kubernetes-preserve-unknown-fields"])
262
+ return "Record<string, any>";
263
+ if (schema.enum && schema.enum.length > 0) {
264
+ return schema.enum.map((v) => JSON.stringify(v)).join(" | ");
265
+ }
266
+ switch (schema.type) {
267
+ case "string":
268
+ return "string";
269
+ case "integer":
270
+ case "number":
271
+ return "number";
272
+ case "boolean":
273
+ return "boolean";
274
+ case "array":
275
+ if (schema.items) {
276
+ const itemType = resolveSchemaType(schema.items);
277
+ if (itemType.includes(" | "))
278
+ return `(${itemType})[]`;
279
+ return `${itemType}[]`;
280
+ }
281
+ return "any[]";
282
+ case "object":
283
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
284
+ const valueType = resolveSchemaType(schema.additionalProperties);
285
+ return `Record<string, ${valueType}>`;
286
+ }
287
+ if (schema.properties)
288
+ return "Record<string, any>"; // will be a property type
289
+ return "Record<string, any>";
290
+ default:
291
+ return "any";
292
+ }
293
+ }
294
+ function extractConstraints(schema) {
295
+ const constraints = {};
296
+ if (schema.minimum !== undefined)
297
+ constraints.minimum = schema.minimum;
298
+ if (schema.maximum !== undefined)
299
+ constraints.maximum = schema.maximum;
300
+ if (schema.minLength !== undefined)
301
+ constraints.minLength = schema.minLength;
302
+ if (schema.maxLength !== undefined)
303
+ constraints.maxLength = schema.maxLength;
304
+ if (schema.pattern !== undefined)
305
+ constraints.pattern = schema.pattern;
306
+ return constraints;
307
+ }
308
+ // ── Utilities ───────────────────────────────────────────────────────
309
+ function pascalCase(str) {
310
+ return str
311
+ .split(/[-_.]/)
312
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
313
+ .join("");
314
+ }
315
+ function singularize(str) {
316
+ if (str.endsWith("ies"))
317
+ return str.slice(0, -3) + "y";
318
+ if (str.endsWith("ses"))
319
+ return str.slice(0, -2);
320
+ if (str.endsWith("s") && !str.endsWith("ss"))
321
+ return str.slice(0, -1);
322
+ return str;
323
+ }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env tsx
2
+ import { validate } from "./validate.js";
3
+ import { printValidationResult } from "@intentius/chant/codegen/validate";
4
+ async function main() {
5
+ const result = await validate();
6
+ printValidationResult(result);
7
+ }
8
+ main().catch((err) => {
9
+ console.error(err);
10
+ process.exit(1);
11
+ });