@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.
- package/dist/actions/index.js +45 -0
- package/dist/api/read-client.js +105 -0
- package/dist/codegen/docs-cli.js +6 -0
- package/dist/codegen/docs.js +115 -0
- package/dist/codegen/generate-cli.js +17 -0
- package/dist/codegen/generate.js +214 -0
- package/dist/codegen/naming.js +164 -0
- package/dist/codegen/package.js +43 -0
- package/dist/composites/catalog.js +1245 -0
- package/dist/composites/cloud-function.js +65 -0
- package/dist/composites/cloud-run-service.js +71 -0
- package/dist/composites/cloud-sql-instance.js +85 -0
- package/dist/composites/gcs-bucket.js +64 -0
- package/dist/composites/gke-cluster.js +113 -0
- package/dist/composites/gke-crdb-region.js +137 -0
- package/dist/composites/gpu-node-pool.js +74 -0
- package/dist/composites/index.js +15 -0
- package/dist/composites/landing-zone.js +89 -0
- package/dist/composites/managed-certificate.js +46 -0
- package/dist/composites/memorystore-redis.js +52 -0
- package/dist/composites/multi-region-vpc.js +108 -0
- package/dist/composites/private-service.js +50 -0
- package/dist/composites/pubsub-pipeline.js +63 -0
- package/dist/composites/secure-project.js +85 -0
- package/dist/composites/vpc-network.js +113 -0
- package/dist/coverage.js +28 -0
- package/dist/deep-observe-hooks.js +152 -0
- package/dist/deep-observe.js +273 -0
- package/dist/default-labels.js +67 -0
- package/dist/describe-resources.js +264 -0
- package/dist/detect.js +16 -0
- package/dist/export-resources.js +59 -0
- package/dist/generated/index.js +1804 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/governance.js +98 -0
- package/dist/import/generator.js +68 -0
- package/dist/import/live-export.js +73 -0
- package/dist/import/parser.js +61 -0
- package/dist/index.js +36 -0
- package/dist/integrity.json +3 -3
- package/dist/lint/audit-catalog.js +38 -0
- package/dist/lint/audit-lineage.js +63 -0
- package/dist/lint/post-synth/gcp-helpers.js +91 -0
- package/dist/lint/post-synth/index.js +54 -0
- package/dist/lint/post-synth/schema-registry.d.ts.map +1 -1
- package/dist/lint/post-synth/schema-registry.js +69 -0
- package/dist/lint/post-synth/wgc101.js +47 -0
- package/dist/lint/post-synth/wgc102.js +29 -0
- package/dist/lint/post-synth/wgc103.js +37 -0
- package/dist/lint/post-synth/wgc104.js +34 -0
- package/dist/lint/post-synth/wgc105.js +39 -0
- package/dist/lint/post-synth/wgc106.js +30 -0
- package/dist/lint/post-synth/wgc107.js +33 -0
- package/dist/lint/post-synth/wgc108.js +34 -0
- package/dist/lint/post-synth/wgc109.js +33 -0
- package/dist/lint/post-synth/wgc110.js +32 -0
- package/dist/lint/post-synth/wgc111.js +47 -0
- package/dist/lint/post-synth/wgc112.js +46 -0
- package/dist/lint/post-synth/wgc113.js +35 -0
- package/dist/lint/post-synth/wgc201.js +30 -0
- package/dist/lint/post-synth/wgc202.js +33 -0
- package/dist/lint/post-synth/wgc203.js +35 -0
- package/dist/lint/post-synth/wgc204.js +33 -0
- package/dist/lint/post-synth/wgc301.js +28 -0
- package/dist/lint/post-synth/wgc302.js +28 -0
- package/dist/lint/post-synth/wgc303.js +29 -0
- package/dist/lint/post-synth/wgc401.js +50 -0
- package/dist/lint/post-synth/wgc402.js +46 -0
- package/dist/lint/post-synth/wgc403.js +74 -0
- package/dist/lint/post-synth/wgc501.js +63 -0
- package/dist/lint/post-synth/wgc502.js +37 -0
- package/dist/lint/post-synth/wgc503.js +49 -0
- package/dist/lint/rules/hardcoded-project.js +51 -0
- package/dist/lint/rules/hardcoded-region.js +51 -0
- package/dist/lint/rules/index.js +3 -0
- package/dist/lint/rules/public-iam.js +37 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +15 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +46 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/floci-gcp.js +49 -0
- package/dist/op/activities/gcp-apply.js +618 -0
- package/dist/op/activities/index.js +10 -0
- package/dist/op/builders.js +50 -0
- package/dist/ownership.js +38 -0
- package/dist/package-cli.js +20 -0
- package/dist/plugin.js +371 -0
- package/dist/pseudo.js +15 -0
- package/dist/rules/schema-registry.ts +2 -1
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +233 -0
- package/dist/spec/fetch.js +107 -0
- package/dist/spec/parse.js +323 -0
- package/dist/validate-cli.js +11 -0
- package/dist/validate.js +24 -0
- package/dist/variables.js +15 -0
- package/package.json +8 -8
- package/src/codegen/package.ts +2 -2
- package/src/lint/post-synth/schema-registry.ts +2 -1
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
- 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
|
+
});
|