@intentius/chant-lexicon-k8s 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 (270) hide show
  1. package/dist/actions/apps.js +18 -0
  2. package/dist/actions/batch.js +6 -0
  3. package/dist/actions/core.js +48 -0
  4. package/dist/actions/index.js +13 -0
  5. package/dist/actions/networking.js +11 -0
  6. package/dist/actions/rbac.js +8 -0
  7. package/dist/api/classify.js +123 -0
  8. package/dist/api/connect.js +62 -0
  9. package/dist/api/fake-cluster.js +175 -0
  10. package/dist/api/operation-surface.d.ts.map +1 -1
  11. package/dist/api/operation-surface.js +109 -0
  12. package/dist/api/owner-chain.js +100 -0
  13. package/dist/api/sweep-types.js +35 -0
  14. package/dist/behaviour-kinds.js +82 -0
  15. package/dist/codegen/docs-cli.js +2 -0
  16. package/dist/codegen/docs.js +146 -0
  17. package/dist/codegen/generate-cli.js +30 -0
  18. package/dist/codegen/generate-lexicon.js +74 -0
  19. package/dist/codegen/generate-list-map-keys.js +88 -0
  20. package/dist/codegen/generate-operations.js +53 -0
  21. package/dist/codegen/generate-typescript.js +124 -0
  22. package/dist/codegen/generate.js +173 -0
  23. package/dist/codegen/naming.js +174 -0
  24. package/dist/codegen/package.js +36 -0
  25. package/dist/codegen/patches.js +88 -0
  26. package/dist/codegen/typecheck.js +4 -0
  27. package/dist/codegen/versions.js +41 -0
  28. package/dist/components/argo-app.js +51 -0
  29. package/dist/components/builders.js +22 -0
  30. package/dist/components/capability-plugin.js +31 -0
  31. package/dist/components/flux-reconcile.js +61 -0
  32. package/dist/components/index.d.ts +1 -1
  33. package/dist/components/index.d.ts.map +1 -1
  34. package/dist/components/index.js +18 -0
  35. package/dist/components/kubectl-apply.d.ts +83 -1
  36. package/dist/components/kubectl-apply.d.ts.map +1 -1
  37. package/dist/components/kubectl-apply.js +274 -0
  38. package/dist/components/kustomize-apply.js +25 -0
  39. package/dist/composites/adot-collector.js +184 -0
  40. package/dist/composites/agic-ingress.js +85 -0
  41. package/dist/composites/aks-external-dns-agent.js +146 -0
  42. package/dist/composites/alb-ingress.js +86 -0
  43. package/dist/composites/argo-app.js +199 -0
  44. package/dist/composites/autoscaled-service.js +174 -0
  45. package/dist/composites/azure-disk-storage-class.js +47 -0
  46. package/dist/composites/azure-file-storage-class.js +48 -0
  47. package/dist/composites/azure-monitor-collector.js +176 -0
  48. package/dist/composites/batch-job.js +139 -0
  49. package/dist/composites/catalog.js +4110 -0
  50. package/dist/composites/cockroachdb-cluster.js +371 -0
  51. package/dist/composites/cockroachdb-region-stack.js +369 -0
  52. package/dist/composites/config-connector-context.js +40 -0
  53. package/dist/composites/configured-app.js +135 -0
  54. package/dist/composites/cron-schedule.js +38 -0
  55. package/dist/composites/cron-workload.js +109 -0
  56. package/dist/composites/ebs-storage-class.js +54 -0
  57. package/dist/composites/efs-storage-class.js +46 -0
  58. package/dist/composites/external-dns-agent.js +122 -0
  59. package/dist/composites/filestore-storage-class.js +49 -0
  60. package/dist/composites/fluent-bit-agent.js +157 -0
  61. package/dist/composites/flux-app.js +156 -0
  62. package/dist/composites/gce-ingress.js +83 -0
  63. package/dist/composites/gce-pd-storage-class.js +49 -0
  64. package/dist/composites/gke-external-dns-agent.js +129 -0
  65. package/dist/composites/gke-fluent-bit-agent.js +165 -0
  66. package/dist/composites/gke-gateway.js +91 -0
  67. package/dist/composites/gke-otel-collector.js +94 -0
  68. package/dist/composites/index.js +51 -0
  69. package/dist/composites/inference-service.js +76 -0
  70. package/dist/composites/irsa-service-account.js +77 -0
  71. package/dist/composites/member-defaults.js +14 -0
  72. package/dist/composites/metrics-server.js +181 -0
  73. package/dist/composites/model.js +92 -0
  74. package/dist/composites/monitored-service.js +152 -0
  75. package/dist/composites/namespace-env.js +114 -0
  76. package/dist/composites/network-isolated-app.js +124 -0
  77. package/dist/composites/node-agent.js +149 -0
  78. package/dist/composites/operator-stack.js +268 -0
  79. package/dist/composites/otel-collector-agent.js +142 -0
  80. package/dist/composites/otel-collector-gateway.js +216 -0
  81. package/dist/composites/otel-collector-node.js +176 -0
  82. package/dist/composites/otel-collector-rbac.js +115 -0
  83. package/dist/composites/otel-collector-shape.js +128 -0
  84. package/dist/composites/otel-collector.js +115 -0
  85. package/dist/composites/otel-operator-collector.js +123 -0
  86. package/dist/composites/ray-cluster.js +378 -0
  87. package/dist/composites/ray-job.js +136 -0
  88. package/dist/composites/ray-service.js +178 -0
  89. package/dist/composites/secure-ingress.js +100 -0
  90. package/dist/composites/security-context.js +1 -0
  91. package/dist/composites/sidecar-app.js +105 -0
  92. package/dist/composites/stateful-app.js +111 -0
  93. package/dist/composites/vllm-serving-runtime.js +114 -0
  94. package/dist/composites/web-app.js +157 -0
  95. package/dist/composites/worker-pool.js +167 -0
  96. package/dist/composites/workload-identity-sa.js +81 -0
  97. package/dist/composites/workload-identity-service-account.js +77 -0
  98. package/dist/config-schema.js +72 -0
  99. package/dist/config.js +59 -0
  100. package/dist/coverage.js +27 -0
  101. package/dist/crd/crd-sources.js +615 -0
  102. package/dist/crd/loader.js +181 -0
  103. package/dist/crd/oci-chart.js +144 -0
  104. package/dist/crd/parser.js +382 -0
  105. package/dist/crd/project-codegen.js +273 -0
  106. package/dist/crd/types.js +7 -0
  107. package/dist/deep-observe-hooks.js +306 -0
  108. package/dist/deep-observe.js +234 -0
  109. package/dist/default-labels.js +87 -0
  110. package/dist/describe-resources.js +699 -0
  111. package/dist/describe-stack-status.js +99 -0
  112. package/dist/detect.js +11 -0
  113. package/dist/effect-receipt-row.js +205 -0
  114. package/dist/export-resources.js +39 -0
  115. package/dist/generated/index.js +1092 -0
  116. package/dist/generated/runtime.js +4 -0
  117. package/dist/group-namespace.js +96 -0
  118. package/dist/import/embedded.js +197 -0
  119. package/dist/import/generator.js +288 -0
  120. package/dist/import/live-export.js +85 -0
  121. package/dist/import/parser.js +204 -0
  122. package/dist/import/testdata/embedded/fixtures.js +60 -0
  123. package/dist/include/entity.js +78 -0
  124. package/dist/include/resolve.js +170 -0
  125. package/dist/index.js +59 -0
  126. package/dist/integrity.json +3 -3
  127. package/dist/kube/apply.js +90 -0
  128. package/dist/kube/connect.js +46 -0
  129. package/dist/kube/delete.js +138 -0
  130. package/dist/kube/describe.js +169 -0
  131. package/dist/kube/events.js +132 -0
  132. package/dist/kube/flags.js +91 -0
  133. package/dist/kube/get.js +159 -0
  134. package/dist/kube/group.js +123 -0
  135. package/dist/kube/logs.js +82 -0
  136. package/dist/kube/project.js +94 -0
  137. package/dist/kube/render.js +227 -0
  138. package/dist/kube/source.js +122 -0
  139. package/dist/kube/target.js +63 -0
  140. package/dist/kube/testing.js +28 -0
  141. package/dist/kube/top.js +99 -0
  142. package/dist/kube/verdict.js +72 -0
  143. package/dist/kube/wait.js +129 -0
  144. package/dist/kustomize/render.js +73 -0
  145. package/dist/kustomize/rendered-entity.js +67 -0
  146. package/dist/kustomize/root.js +68 -0
  147. package/dist/lint/audit-catalog.js +61 -0
  148. package/dist/lint/audit-lineage.js +118 -0
  149. package/dist/lint/post-synth/argo-helpers.js +36 -0
  150. package/dist/lint/post-synth/argo002.js +31 -0
  151. package/dist/lint/post-synth/argo003.js +53 -0
  152. package/dist/lint/post-synth/argo005.js +54 -0
  153. package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
  154. package/dist/lint/post-synth/crd-schema-helpers.js +214 -0
  155. package/dist/lint/post-synth/flux002.js +56 -0
  156. package/dist/lint/post-synth/flux003.js +50 -0
  157. package/dist/lint/post-synth/index.js +104 -0
  158. package/dist/lint/post-synth/k8s-helpers.js +186 -0
  159. package/dist/lint/post-synth/monitoring-selection-helpers.js +163 -0
  160. package/dist/lint/post-synth/otel-placement-helpers.js +293 -0
  161. package/dist/lint/post-synth/sops-helpers.js +43 -0
  162. package/dist/lint/post-synth/wk8005.js +44 -0
  163. package/dist/lint/post-synth/wk8006.js +56 -0
  164. package/dist/lint/post-synth/wk8041.js +60 -0
  165. package/dist/lint/post-synth/wk8042.js +36 -0
  166. package/dist/lint/post-synth/wk8101.js +52 -0
  167. package/dist/lint/post-synth/wk8102.js +30 -0
  168. package/dist/lint/post-synth/wk8103.js +33 -0
  169. package/dist/lint/post-synth/wk8104.js +58 -0
  170. package/dist/lint/post-synth/wk8105.js +33 -0
  171. package/dist/lint/post-synth/wk8201.js +44 -0
  172. package/dist/lint/post-synth/wk8202.js +34 -0
  173. package/dist/lint/post-synth/wk8203.js +34 -0
  174. package/dist/lint/post-synth/wk8204.js +71 -0
  175. package/dist/lint/post-synth/wk8205.js +38 -0
  176. package/dist/lint/post-synth/wk8207.js +33 -0
  177. package/dist/lint/post-synth/wk8208.js +33 -0
  178. package/dist/lint/post-synth/wk8209.js +33 -0
  179. package/dist/lint/post-synth/wk8301.js +49 -0
  180. package/dist/lint/post-synth/wk8302.js +34 -0
  181. package/dist/lint/post-synth/wk8303.js +68 -0
  182. package/dist/lint/post-synth/wk8304.js +58 -0
  183. package/dist/lint/post-synth/wk8305.js +96 -0
  184. package/dist/lint/post-synth/wk8306.js +38 -0
  185. package/dist/lint/post-synth/wk8401.js +86 -0
  186. package/dist/lint/post-synth/wk8402.js +31 -0
  187. package/dist/lint/post-synth/wk8403.js +49 -0
  188. package/dist/lint/post-synth/wk8404.js +36 -0
  189. package/dist/lint/post-synth/wk8405.js +65 -0
  190. package/dist/lint/post-synth/wk8406.js +50 -0
  191. package/dist/lint/post-synth/wk8407.js +58 -0
  192. package/dist/lint/post-synth/wk8501.js +36 -0
  193. package/dist/lint/post-synth/wk8502.js +35 -0
  194. package/dist/lint/post-synth/wk8503.js +164 -0
  195. package/dist/lint/post-synth/wk8504.js +20 -0
  196. package/dist/lint/post-synth/wk8505.js +31 -0
  197. package/dist/lint/post-synth/wk8601.js +40 -0
  198. package/dist/lint/post-synth/wk8602.js +120 -0
  199. package/dist/lint/post-synth/wk8603.js +63 -0
  200. package/dist/lint/post-synth/wk8604.js +31 -0
  201. package/dist/lint/post-synth/wk8605.js +124 -0
  202. package/dist/lint/post-synth/wk8606.js +22 -0
  203. package/dist/lint/post-synth/wk8701.js +31 -0
  204. package/dist/lint/post-synth/wk8702.js +37 -0
  205. package/dist/lint/post-synth/wk8703.js +31 -0
  206. package/dist/lint/post-synth/wk8704.js +135 -0
  207. package/dist/lint/post-synth/wk8705.js +38 -0
  208. package/dist/lint/rules/argo-appset-single-project.js +56 -0
  209. package/dist/lint/rules/argo-ast.js +94 -0
  210. package/dist/lint/rules/argo-automated-prune.js +59 -0
  211. package/dist/lint/rules/flux-source-ref-pin.js +44 -0
  212. package/dist/lint/rules/hardcoded-namespace.js +57 -0
  213. package/dist/lint/rules/latest-image-tag.js +103 -0
  214. package/dist/lint/rules/missing-resource-limits.js +85 -0
  215. package/dist/lsp/completions.d.ts.map +1 -1
  216. package/dist/lsp/completions.js +18 -0
  217. package/dist/lsp/hover.d.ts.map +1 -1
  218. package/dist/lsp/hover.js +57 -0
  219. package/dist/manifest-entity.js +71 -0
  220. package/dist/manifest.json +1 -1
  221. package/dist/micro-time.js +49 -0
  222. package/dist/op/activities/apply-diff.d.ts +31 -0
  223. package/dist/op/activities/apply-diff.d.ts.map +1 -0
  224. package/dist/op/activities/apply-diff.js +79 -0
  225. package/dist/op/activities/argo.js +98 -0
  226. package/dist/op/activities/ensure-secret.js +38 -0
  227. package/dist/op/activities/index.js +51 -0
  228. package/dist/op/activities/kubectl.d.ts +64 -0
  229. package/dist/op/activities/kubectl.d.ts.map +1 -1
  230. package/dist/op/activities/kubectl.js +517 -0
  231. package/dist/op/activities/wait-for-ready.js +229 -0
  232. package/dist/op/builders.js +68 -0
  233. package/dist/package-cli.js +21 -0
  234. package/dist/patch/json-patch.js +187 -0
  235. package/dist/patch/rendered-patch.js +94 -0
  236. package/dist/plugin.js +741 -0
  237. package/dist/project-kinds.js +56 -0
  238. package/dist/receipt-store.js +216 -0
  239. package/dist/rules/crd-schema-helpers.ts +2 -1
  240. package/dist/secret-labels.js +33 -0
  241. package/dist/secret-store.js +116 -0
  242. package/dist/serializer.d.ts.map +1 -1
  243. package/dist/serializer.js +578 -0
  244. package/dist/sops/encrypted-secret-file.js +194 -0
  245. package/dist/sops/entity.js +32 -0
  246. package/dist/spec/fetch.js +58 -0
  247. package/dist/spec/parse.js +622 -0
  248. package/dist/subscribe-changes.js +168 -0
  249. package/dist/teardown.js +197 -0
  250. package/dist/telemetry-release.js +86 -0
  251. package/dist/validate-cli.js +17 -0
  252. package/dist/validate.js +47 -0
  253. package/dist/variables.js +34 -0
  254. package/dist/whoami.js +109 -0
  255. package/package.json +12 -12
  256. package/src/api/operation-surface.ts +2 -1
  257. package/src/codegen/package.ts +2 -2
  258. package/src/components/index.ts +3 -0
  259. package/src/components/kubectl-apply-plan.k3d.e2e.test.ts +127 -0
  260. package/src/components/kubectl-apply.test.ts +262 -0
  261. package/src/components/kubectl-apply.ts +341 -12
  262. package/src/deep-observe-hooks.ts +2 -2
  263. package/src/import/roundtrip-build.test.ts +46 -0
  264. package/src/lint/post-synth/crd-schema-helpers.ts +2 -1
  265. package/src/lsp/completions.ts +2 -1
  266. package/src/lsp/hover.ts +2 -1
  267. package/src/op/activities/apply-diff.ts +87 -0
  268. package/src/op/activities/kubectl-plan.test.ts +224 -0
  269. package/src/op/activities/kubectl.ts +204 -16
  270. package/src/serializer.ts +2 -1
@@ -0,0 +1,382 @@
1
+ /**
2
+ * CRD parser — converts CRD YAML into K8sParseResult entries.
3
+ *
4
+ * Parses the openAPIV3Schema from a CRD's versions to extract resource
5
+ * properties, building the same K8sParseResult structures used by the
6
+ * main K8s swagger parser. This enables CRD-based resources to integrate
7
+ * with the full codegen pipeline.
8
+ */
9
+ import { namespaceSegmentForGroup } from "../group-namespace.js";
10
+ import { loadAll } from "js-yaml";
11
+ /**
12
+ * Parse a CRD YAML document string into K8sParseResult entries.
13
+ * Returns one result per served version in the CRD.
14
+ */
15
+ export function parseCRD(content) {
16
+ const results = [];
17
+ // Use js-yaml to handle full YAML spec (CRD YAMLs use same-indent arrays,
18
+ // nested block scalars, and deep nesting not supported by the lightweight parser).
19
+ const documents = [];
20
+ loadAll(content, (doc) => documents.push(doc));
21
+ for (const doc of documents) {
22
+ if (!doc || typeof doc !== "object")
23
+ continue;
24
+ const docObj = doc;
25
+ if (docObj.kind !== "CustomResourceDefinition")
26
+ continue;
27
+ const spec = docObj.spec;
28
+ if (!spec?.group || !spec?.names?.kind || !spec?.versions)
29
+ continue;
30
+ const crdResults = parseCRDSpec(spec);
31
+ results.push(...crdResults);
32
+ }
33
+ return results;
34
+ }
35
+ /**
36
+ * The version a CRD's types are generated from: the served storage version,
37
+ * else the first served one.
38
+ */
39
+ export function crdTargetVersion(spec) {
40
+ return spec.versions.find((v) => v.storage && v.served) ?? spec.versions.find((v) => v.served);
41
+ }
42
+ /**
43
+ * Parse a CRD spec into K8sParseResult entries.
44
+ * Extracts one result per served version with storage version preferred.
45
+ */
46
+ export function parseCRDSpec(spec) {
47
+ const results = [];
48
+ const groupNs = namespaceSegmentForGroup(spec.group);
49
+ const targetVersion = crdTargetVersion(spec);
50
+ if (!targetVersion)
51
+ return results;
52
+ const typeName = `K8s::${groupNs}::${spec.names.kind}`;
53
+ const gvk = {
54
+ group: spec.group,
55
+ version: targetVersion.name,
56
+ kind: spec.names.kind,
57
+ };
58
+ const schema = targetVersion.schema?.openAPIV3Schema;
59
+ const properties = schema ? extractProperties(schema) : [];
60
+ const propertyTypes = schema ? extractPropertyTypes(schema, typeName) : [];
61
+ const status = schema ? extractStatusType(schema, typeName) : {};
62
+ const specSchema = schema?.properties?.spec ? toFieldSchema(schema.properties.spec) : undefined;
63
+ const attributes = [
64
+ { name: "name", tsType: "string" },
65
+ { name: "namespace", tsType: "string" },
66
+ { name: "uid", tsType: "string" },
67
+ ];
68
+ if (status.attribute)
69
+ attributes.push(status.attribute);
70
+ results.push({
71
+ resource: {
72
+ typeName,
73
+ description: `Custom resource: ${spec.names.kind} (${spec.group})`,
74
+ properties,
75
+ attributes,
76
+ deprecatedProperties: [],
77
+ },
78
+ propertyTypes: status.propertyType ? [...propertyTypes, status.propertyType] : propertyTypes,
79
+ enums: [],
80
+ gvk,
81
+ // chant #1372 — the spec's field schema, shipped in the lexicon JSON so
82
+ // the post-synth checks (WK8501/WK8502) can validate a custom resource
83
+ // the way the API server's structural schema would, before apply.
84
+ ...(specSchema ? { specSchema } : {}),
85
+ // chant #1074 — the CRD declares its own plural and scope, so the
86
+ // operation surface for a custom resource comes from the same document its
87
+ // types do, exactly as the OpenAPI `paths` supply them for built-in kinds.
88
+ operation: {
89
+ plural: spec.names.plural,
90
+ scope: spec.scope ?? "Namespaced",
91
+ verbs: ["delete", "get", "list", "patch", "post", "put", "watch"],
92
+ },
93
+ });
94
+ return results;
95
+ }
96
+ // ── Property extraction ─────────────────────────────────────────────
97
+ /**
98
+ * Extract top-level properties from a CRD's openAPIV3Schema.
99
+ * Focuses on the "spec" sub-object and metadata, skipping status.
100
+ */
101
+ function extractProperties(schema) {
102
+ const result = [];
103
+ const topProps = schema.properties ?? {};
104
+ const topRequired = new Set(schema.required ?? []);
105
+ // Skip apiVersion, kind, status — same pattern as core parser. `metadata` is
106
+ // skipped here too and re-added below: every custom resource carries the full
107
+ // ObjectMeta whatever its own schema says, and most CRD schemas say nothing.
108
+ const skipProps = new Set(["apiVersion", "kind", "metadata", "status"]);
109
+ // A CRD's openAPIV3Schema describes what the *controller* reads, so authors
110
+ // routinely declare only `spec` and `status` — the Fabric8 CRDGenerator emits
111
+ // exactly that, and so do most hand-written CRDs. The API server still accepts
112
+ // (and requires) the standard `metadata`, so deriving the constructor surface
113
+ // from the schema alone produced a class with no way to set a name or
114
+ // namespace, and every call site needed an `as any` to get one on. Emit it
115
+ // unconditionally, typed the same as every built-in kind's.
116
+ result.push({
117
+ name: "metadata",
118
+ tsType: "ObjectMeta",
119
+ required: false,
120
+ description: "Standard object's metadata.",
121
+ constraints: {},
122
+ });
123
+ for (const [name, prop] of Object.entries(topProps)) {
124
+ if (skipProps.has(name))
125
+ continue;
126
+ result.push({
127
+ name,
128
+ tsType: resolveSchemaType(prop),
129
+ required: topRequired.has(name),
130
+ description: prop.description,
131
+ enum: prop.enum,
132
+ constraints: extractConstraints(prop),
133
+ });
134
+ }
135
+ return result;
136
+ }
137
+ /**
138
+ * Extract nested object types as ParsedPropertyType entries.
139
+ * Walks the spec's properties looking for inline object definitions.
140
+ */
141
+ function extractPropertyTypes(schema, parentTypeName) {
142
+ const results = [];
143
+ const specSchema = schema.properties?.spec;
144
+ if (!specSchema?.properties)
145
+ return results;
146
+ // Use the short name (last :: segment) as prefix so the naming pipeline
147
+ // produces valid TS identifiers: "RayCluster_AutoscalerOptions" not
148
+ // "K8s::Ray::RayCluster::AutoscalerOptions".
149
+ const shortName = parentTypeName.split("::").pop();
150
+ // Property-type identifiers must be unique within a resource. Singularizing
151
+ // array names can collide a sibling scalar object — e.g. Argo Application has
152
+ // both `source` (object) and `sources` (array), which both reduce to
153
+ // `Application_Source`. Track emitted names and fall back to the raw
154
+ // (un-singularized) name, then a numeric suffix, on collision.
155
+ const usedNames = new Set();
156
+ const uniqueName = (base, raw) => {
157
+ if (!usedNames.has(base)) {
158
+ usedNames.add(base);
159
+ return base;
160
+ }
161
+ const rawCandidate = `${shortName}_${pascalCase(raw)}`;
162
+ if (!usedNames.has(rawCandidate)) {
163
+ usedNames.add(rawCandidate);
164
+ return rawCandidate;
165
+ }
166
+ let i = 2;
167
+ while (usedNames.has(`${base}${i}`))
168
+ i++;
169
+ const suffixed = `${base}${i}`;
170
+ usedNames.add(suffixed);
171
+ return suffixed;
172
+ };
173
+ for (const [name, prop] of Object.entries(specSchema.properties)) {
174
+ // Extract inline object definitions as property types
175
+ if (prop.type === "object" && prop.properties) {
176
+ const ptName = uniqueName(`${shortName}_${pascalCase(name)}`, name);
177
+ const requiredSet = new Set(prop.required ?? []);
178
+ results.push({
179
+ name: ptName,
180
+ defType: name,
181
+ properties: Object.entries(prop.properties).map(([pName, pSchema]) => ({
182
+ name: pName,
183
+ tsType: resolveSchemaType(pSchema),
184
+ required: requiredSet.has(pName),
185
+ description: pSchema.description,
186
+ enum: pSchema.enum,
187
+ constraints: extractConstraints(pSchema),
188
+ })),
189
+ });
190
+ }
191
+ // Array of objects
192
+ if (prop.type === "array" && prop.items?.type === "object" && prop.items.properties) {
193
+ const itemSchema = prop.items;
194
+ const itemProps = itemSchema.properties;
195
+ const ptName = uniqueName(`${shortName}_${pascalCase(singularize(name))}`, name);
196
+ const requiredSet = new Set(itemSchema.required ?? []);
197
+ results.push({
198
+ name: ptName,
199
+ defType: name,
200
+ properties: Object.entries(itemProps).map(([pName, pSchema]) => ({
201
+ name: pName,
202
+ tsType: resolveSchemaType(pSchema),
203
+ required: requiredSet.has(pName),
204
+ description: pSchema.description,
205
+ enum: pSchema.enum,
206
+ constraints: extractConstraints(pSchema),
207
+ })),
208
+ });
209
+ }
210
+ }
211
+ return results;
212
+ }
213
+ /**
214
+ * Extract the read-only `status` sub-object as a property type plus a `status`
215
+ * attribute on the resource. Status is never part of the writable spec surface
216
+ * (it is server-owned), so it is exposed only as a read-only output.
217
+ *
218
+ * Shallow by design, mirroring extractPropertyTypes: scalar leaves are typed,
219
+ * nested objects and arrays-of-objects degrade to `Record<string, any>`. Status
220
+ * with `x-kubernetes-preserve-unknown-fields` (or no properties) degrades to a
221
+ * bare `Record<string, any>` attribute with no property type.
222
+ */
223
+ function extractStatusType(schema, parentTypeName) {
224
+ const statusSchema = schema.properties?.status;
225
+ if (!statusSchema)
226
+ return {};
227
+ // The `.d.ts` accessor is deliberately opaque, like the constructor's
228
+ // `spec: Record<string, unknown>`. The writable side is typed through the
229
+ // lexicon JSON instead (`specSchema`, chant #1372, checked by WK8501/WK8502).
230
+ // Status is server-owned and never validated; the property type below only
231
+ // registers its name so the shape is discoverable (chant #1372 reviewed the
232
+ // claim that it fed LSP / validation — it did not, and does not need to).
233
+ const attribute = { name: "status", tsType: "Record<string, unknown>" };
234
+ // Opaque status (preserve-unknown or no schema) → read-only record only.
235
+ if (statusSchema["x-kubernetes-preserve-unknown-fields"] || !statusSchema.properties) {
236
+ return { attribute };
237
+ }
238
+ const shortName = parentTypeName.split("::").pop();
239
+ const propertyType = {
240
+ name: `${shortName}_Status`,
241
+ defType: "status",
242
+ properties: Object.entries(statusSchema.properties).map(([pName, pSchema]) => ({
243
+ name: pName,
244
+ tsType: resolveSchemaType(pSchema),
245
+ required: new Set(statusSchema.required ?? []).has(pName),
246
+ description: pSchema.description,
247
+ enum: pSchema.enum,
248
+ constraints: extractConstraints(pSchema),
249
+ })),
250
+ };
251
+ return { attribute, propertyType };
252
+ }
253
+ // ── Spec field schema (chant #1372) ────────────────────────────────
254
+ /**
255
+ * Reduce an `openAPIV3Schema` node to the compact {@link CrdFieldSchema} the
256
+ * lexicon JSON ships. Walks the whole tree; drops descriptions, formats,
257
+ * defaults and numeric bounds. `allOf`/`oneOf`/`anyOf` are not modelled — a
258
+ * node that relies on them with no `type` of its own comes out untyped, which
259
+ * the validators treat as "anything goes".
260
+ */
261
+ export function toFieldSchema(node) {
262
+ const out = {};
263
+ if (node["x-kubernetes-int-or-string"]) {
264
+ out.open = true;
265
+ return out;
266
+ }
267
+ const type = node.type;
268
+ if (type === "string" || type === "integer" || type === "number" || type === "boolean" || type === "object" || type === "array") {
269
+ out.type = type;
270
+ }
271
+ else if (node.properties) {
272
+ out.type = "object";
273
+ }
274
+ else if (node.items) {
275
+ out.type = "array";
276
+ }
277
+ if (node.enum && node.enum.length > 0) {
278
+ out.enum = node.enum.map((v) => String(v));
279
+ }
280
+ if (out.type === "object") {
281
+ if (node.properties) {
282
+ const fields = {};
283
+ for (const [name, child] of Object.entries(node.properties)) {
284
+ fields[name] = toFieldSchema(child);
285
+ }
286
+ out.fields = fields;
287
+ }
288
+ if (node.required && node.required.length > 0) {
289
+ out.required = [...node.required];
290
+ }
291
+ if (node["x-kubernetes-preserve-unknown-fields"] || node.additionalProperties || !node.properties) {
292
+ out.open = true;
293
+ }
294
+ }
295
+ else if (out.type === "array") {
296
+ if (node.items)
297
+ out.items = toFieldSchema(node.items);
298
+ }
299
+ else if (node["x-kubernetes-preserve-unknown-fields"] && !out.type) {
300
+ out.open = true;
301
+ }
302
+ return out;
303
+ }
304
+ /**
305
+ * Resolve an OpenAPI schema node to a TypeScript type string.
306
+ */
307
+ function resolveSchemaType(schema) {
308
+ if (!schema)
309
+ return "any";
310
+ if (schema["x-kubernetes-int-or-string"])
311
+ return "string | number";
312
+ if (schema["x-kubernetes-preserve-unknown-fields"])
313
+ return "Record<string, any>";
314
+ if (schema.enum && schema.enum.length > 0) {
315
+ return schema.enum.map((v) => JSON.stringify(v)).join(" | ");
316
+ }
317
+ switch (schema.type) {
318
+ case "string":
319
+ return "string";
320
+ case "integer":
321
+ case "number":
322
+ return "number";
323
+ case "boolean":
324
+ return "boolean";
325
+ case "array":
326
+ if (schema.items) {
327
+ const itemType = resolveSchemaType(schema.items);
328
+ if (itemType.includes(" | "))
329
+ return `(${itemType})[]`;
330
+ return `${itemType}[]`;
331
+ }
332
+ return "any[]";
333
+ case "object":
334
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
335
+ const valueType = resolveSchemaType(schema.additionalProperties);
336
+ return `Record<string, ${valueType}>`;
337
+ }
338
+ if (schema.properties)
339
+ return "Record<string, any>"; // will be a property type
340
+ return "Record<string, any>";
341
+ default:
342
+ return "any";
343
+ }
344
+ }
345
+ /**
346
+ * Extract property constraints from an OpenAPI schema node.
347
+ */
348
+ function extractConstraints(schema) {
349
+ const constraints = {};
350
+ if (schema.minimum !== undefined)
351
+ constraints.minimum = schema.minimum;
352
+ if (schema.maximum !== undefined)
353
+ constraints.maximum = schema.maximum;
354
+ if (schema.minLength !== undefined)
355
+ constraints.minLength = schema.minLength;
356
+ if (schema.maxLength !== undefined)
357
+ constraints.maxLength = schema.maxLength;
358
+ if (schema.pattern !== undefined)
359
+ constraints.pattern = schema.pattern;
360
+ return constraints;
361
+ }
362
+ /**
363
+ * Convert a string to PascalCase.
364
+ */
365
+ function pascalCase(str) {
366
+ return str
367
+ .split(/[-_.]/)
368
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
369
+ .join("");
370
+ }
371
+ /**
372
+ * Naive singularize — removes trailing "s" for property type naming.
373
+ */
374
+ function singularize(str) {
375
+ if (str.endsWith("ies"))
376
+ return str.slice(0, -3) + "y";
377
+ if (str.endsWith("ses"))
378
+ return str.slice(0, -2);
379
+ if (str.endsWith("s") && !str.endsWith("ss"))
380
+ return str.slice(0, -1);
381
+ return str;
382
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Project-local CRD classes: the k8s lexicon's `projectCodegen` hook
3
+ * (`chant generate`, core's project-codegen.ts).
4
+ *
5
+ * A project lists CRD sources under `k8s.crds` in `chant.config.ts`. Generation
6
+ * parses each CRD with the same parser the lexicon's own CRD_SOURCES go
7
+ * through (./parser.ts), so the entity type, GVK and spec field schema match
8
+ * what a shipped CRD gets, and writes two files to `<outDir>/k8s/`:
9
+ *
10
+ * - `index.ts`: a typed class per kind. Its props are typed from the CRD's
11
+ * `openAPIV3Schema` (a shipped CRD's `.d.ts` types `spec` as a record; a
12
+ * project's own types it fully). The module registers its kinds with
13
+ * ../project-kinds.ts when imported.
14
+ * - `kinds.json`: each kind's GVK and spec field schema, which a build loads
15
+ * (`load`) so the serializer and the WK8501/WK8502 spec checks know the
16
+ * kinds whatever process imported the module.
17
+ */
18
+ import { createHash } from "node:crypto";
19
+ import { existsSync, readFileSync } from "node:fs";
20
+ import { join, resolve } from "node:path";
21
+ import { loadAll } from "js-yaml";
22
+ import { GENERATED_TS_HEADER } from "@intentius/chant/project-codegen";
23
+ import { jsdocLines, schemaToTypeScript, tsPropertyKey } from "@intentius/chant/codegen/json-schema-to-ts";
24
+ import { fetchWithRetry } from "@intentius/chant/codegen/fetch";
25
+ import { namespaceSegmentForGroup } from "../group-namespace.js";
26
+ import { registerProjectKinds } from "../project-kinds.js";
27
+ import { gvkToApiVersion } from "../spec/parse.js";
28
+ import { fetchCRDContent } from "./loader.js";
29
+ import { crdTargetVersion, parseCRDSpec } from "./parser.js";
30
+ /** The registry file written beside the module. */
31
+ export const KINDS_FILE = "kinds.json";
32
+ /** Fields every resource class handles itself; a CRD's own schema for them is ignored. */
33
+ const RESERVED_FIELDS = new Set(["apiVersion", "kind", "metadata", "status"]);
34
+ /** The hook, with injectable readers. */
35
+ export function k8sProjectCodegen(readers = {}) {
36
+ return {
37
+ inputs: (ctx) => {
38
+ const sources = declaredSources(ctx);
39
+ if (!sources)
40
+ return undefined;
41
+ return sources.map((source, i) => describeSource(source, i, ctx.projectRoot));
42
+ },
43
+ async generate(ctx) {
44
+ const sources = declaredSources(ctx) ?? [];
45
+ const fetchUrl = readers.fetchUrl ??
46
+ (async (url) => {
47
+ const response = ctx.fetch ? await ctx.fetch(url) : await fetchWithRetry(url);
48
+ if (!response.ok)
49
+ throw new Error(`${url}: HTTP ${response.status}`);
50
+ return response.text();
51
+ });
52
+ const readChart = readers.readChart ??
53
+ ((s) => fetchCRDContent({ type: "helm", chart: s.chart, version: s.version, digest: s.digest, chartSubdir: s.chartSubdir }));
54
+ const kinds = [];
55
+ for (const [i, source] of sources.entries()) {
56
+ describeSource(source, i, ctx.projectRoot); // validates the pin before any fetch
57
+ let content;
58
+ if (source.type === "file") {
59
+ content = readFileSync(resolve(ctx.projectRoot, source.path), "utf8");
60
+ }
61
+ else if (source.type === "url") {
62
+ content = await fetchUrl(source.url);
63
+ const actual = sha256(content);
64
+ if (actual !== normalizeSha(source.sha256)) {
65
+ throw new Error(`k8s.crds[${i}]: ${source.url} has sha256 ${actual}, but the config pins ${normalizeSha(source.sha256)}. ` +
66
+ "Check the URL points at a fixed release, then update the pin.");
67
+ }
68
+ }
69
+ else {
70
+ content = await readChart(source);
71
+ }
72
+ const found = crdKinds(content, source.kinds);
73
+ if (found.length === 0) {
74
+ throw new Error(`k8s.crds[${i}] (${sourceLabel(source)}) holds no CustomResourceDefinition${source.kinds ? ` of kind ${source.kinds.join(", ")}` : ""}`);
75
+ }
76
+ kinds.push(...found);
77
+ }
78
+ assignClassNames(kinds);
79
+ return {
80
+ files: { "index.ts": renderModule(kinds), [KINDS_FILE]: renderKinds(kinds) },
81
+ summary: kinds.map((k) => `${k.className} (${k.apiVersion}, ${k.scope === "Cluster" ? "cluster-scoped" : "namespaced"})`),
82
+ };
83
+ },
84
+ load(ctx) {
85
+ const path = join(ctx.outDir, KINDS_FILE);
86
+ if (!existsSync(path))
87
+ return;
88
+ registerProjectKinds(JSON.parse(readFileSync(path, "utf8")));
89
+ },
90
+ };
91
+ }
92
+ function declaredSources(ctx) {
93
+ const k8s = ctx.config.k8s;
94
+ return k8s?.crds && k8s.crds.length > 0 ? k8s.crds : undefined;
95
+ }
96
+ /**
97
+ * A source as a JSON value for the drift digest: a file by the hash of its
98
+ * content, a remote source by its pin. Throws on an unpinned remote source
99
+ * or a missing file.
100
+ */
101
+ function describeSource(source, i, projectRoot) {
102
+ const kinds = source.kinds ? { kinds: [...source.kinds].sort() } : {};
103
+ switch (source.type) {
104
+ case "file": {
105
+ const path = resolve(projectRoot, source.path);
106
+ if (!existsSync(path))
107
+ throw new Error(`k8s.crds[${i}]: CRD file not found: ${source.path}`);
108
+ return { type: "file", path: source.path, sha256: sha256(readFileSync(path)), ...kinds };
109
+ }
110
+ case "url":
111
+ if (typeof source.sha256 !== "string" || !/^(sha256:)?[0-9a-f]{64}$/.test(source.sha256)) {
112
+ throw new Error(`k8s.crds[${i}]: ${source.url} needs a sha256 pin of its content. An unpinned URL makes the generated classes depend on when they were generated.`);
113
+ }
114
+ return { type: "url", url: source.url, sha256: normalizeSha(source.sha256), ...kinds };
115
+ case "helm":
116
+ if (!source.version) {
117
+ throw new Error(`k8s.crds[${i}]: chart ${source.chart} needs a version. An unpinned chart makes the generated classes depend on when they were generated.`);
118
+ }
119
+ return {
120
+ type: "helm",
121
+ chart: source.chart,
122
+ version: source.version,
123
+ ...(source.digest ? { digest: source.digest } : {}),
124
+ ...(source.chartSubdir ? { chartSubdir: source.chartSubdir } : {}),
125
+ ...kinds,
126
+ };
127
+ default:
128
+ throw new Error(`k8s.crds[${i}]: unsupported source type ${JSON.stringify(source.type)}`);
129
+ }
130
+ }
131
+ function sourceLabel(source) {
132
+ return source.type === "file" ? source.path : source.type === "url" ? source.url : `${source.chart} ${source.version}`;
133
+ }
134
+ function sha256(content) {
135
+ return createHash("sha256").update(content).digest("hex");
136
+ }
137
+ function normalizeSha(pin) {
138
+ return pin.replace(/^sha256:/, "");
139
+ }
140
+ /** Every CRD in a (multi-document) YAML text, filtered to `allow` when given. */
141
+ function crdKinds(content, allow) {
142
+ const docs = [];
143
+ loadAll(content, (doc) => docs.push(doc));
144
+ const out = [];
145
+ for (const doc of docs) {
146
+ if (!doc || typeof doc !== "object")
147
+ continue;
148
+ const d = doc;
149
+ if (d.kind !== "CustomResourceDefinition" || !d.spec?.group || !d.spec.names?.kind || !d.spec.versions)
150
+ continue;
151
+ if (allow && allow.length > 0 && !allow.includes(d.spec.names.kind))
152
+ continue;
153
+ const [parsed] = parseCRDSpec(d.spec);
154
+ const version = crdTargetVersion(d.spec);
155
+ if (!parsed || !version)
156
+ continue;
157
+ out.push({
158
+ resourceType: parsed.resource.typeName,
159
+ apiVersion: gvkToApiVersion(parsed.gvk),
160
+ kind: parsed.gvk.kind,
161
+ group: d.spec.group,
162
+ scope: d.spec.scope ?? "Namespaced",
163
+ attributes: parsed.resource.attributes,
164
+ ...(parsed.specSchema ? { specSchema: parsed.specSchema } : {}),
165
+ schema: version.schema?.openAPIV3Schema,
166
+ className: parsed.gvk.kind,
167
+ });
168
+ }
169
+ return out;
170
+ }
171
+ /**
172
+ * Class names are the kind. Two kinds of the same name from different groups
173
+ * both take the group's namespace segment as a prefix (`CertManagerCertificate`);
174
+ * the same group and kind twice is an error.
175
+ */
176
+ function assignClassNames(kinds) {
177
+ const seen = new Set();
178
+ for (const k of kinds) {
179
+ const key = `${k.group}/${k.kind}`;
180
+ if (seen.has(key))
181
+ throw new Error(`k8s.crds declares ${k.kind} (${k.group}) more than once`);
182
+ seen.add(key);
183
+ }
184
+ const byKind = new Map();
185
+ for (const k of kinds)
186
+ byKind.set(k.kind, [...(byKind.get(k.kind) ?? []), k]);
187
+ for (const group of byKind.values()) {
188
+ if (group.length < 2)
189
+ continue;
190
+ for (const k of group)
191
+ k.className = `${namespaceSegmentForGroup(k.group).replace(/[^A-Za-z0-9]/g, "")}${k.kind}`;
192
+ }
193
+ kinds.sort((a, b) => a.className.localeCompare(b.className));
194
+ }
195
+ function renderKinds(kinds) {
196
+ const entries = kinds.map((k) => ({
197
+ resourceType: k.resourceType,
198
+ apiVersion: k.apiVersion,
199
+ kind: k.kind,
200
+ ...(k.specSchema ? { specSchema: k.specSchema } : {}),
201
+ }));
202
+ return JSON.stringify(entries, null, 2) + "\n";
203
+ }
204
+ /**
205
+ * The metadata a manifest author sets. Declared in the module rather than
206
+ * imported: the lexicon's `ObjectMeta` export is a runtime constructor, and
207
+ * its typed declaration is not reachable from every install.
208
+ */
209
+ const OBJECT_META = [
210
+ "/** Standard object metadata: the fields a manifest sets. */",
211
+ "export interface ObjectMeta {",
212
+ " name?: string;",
213
+ " generateName?: string;",
214
+ " namespace?: string;",
215
+ " labels?: Record<string, string>;",
216
+ " annotations?: Record<string, string>;",
217
+ " finalizers?: string[];",
218
+ " ownerReferences?: Array<{",
219
+ " apiVersion: string;",
220
+ " kind: string;",
221
+ " name: string;",
222
+ " uid: string;",
223
+ " controller?: boolean;",
224
+ " blockOwnerDeletion?: boolean;",
225
+ " }>;",
226
+ "}",
227
+ ];
228
+ function renderModule(kinds) {
229
+ const lines = [
230
+ GENERATED_TS_HEADER,
231
+ "// Classes for the custom resources k8s.crds declares in chant.config.",
232
+ 'import type { Declarable } from "@intentius/chant";',
233
+ 'import { createResource } from "@intentius/chant/runtime";',
234
+ 'import { registerProjectKinds } from "@intentius/chant-lexicon-k8s";',
235
+ "",
236
+ "registerProjectKinds([",
237
+ ...kinds.map((k) => ` ${JSON.stringify({ resourceType: k.resourceType, apiVersion: k.apiVersion, kind: k.kind })},`),
238
+ "]);",
239
+ "",
240
+ ...OBJECT_META,
241
+ ];
242
+ for (const k of kinds) {
243
+ const name = k.className;
244
+ const properties = (k.schema?.properties ?? {});
245
+ const required = new Set(k.schema?.required ?? []);
246
+ const fields = Object.keys(properties).filter((f) => !RESERVED_FIELDS.has(f));
247
+ const members = [" /** Standard object's metadata. */", " metadata?: ObjectMeta;"];
248
+ for (const field of fields) {
249
+ const typeName = `${name}${pascal(field)}`;
250
+ const { type, declarations } = schemaToTypeScript(properties[field], {
251
+ openByDefault: false,
252
+ namePrefix: typeName,
253
+ });
254
+ for (const d of declarations) {
255
+ lines.push("", ...jsdocLines(d.description, ""), `export type ${d.name} = ${d.type};`);
256
+ }
257
+ lines.push("", `/** The \`${field}\` of a ${k.kind} (${k.apiVersion}). */`, `export type ${typeName} = ${type};`);
258
+ members.push(...jsdocLines(properties[field].description, " "));
259
+ members.push(` ${tsPropertyKey(field)}${required.has(field) ? "" : "?"}: ${typeName};`);
260
+ }
261
+ const attrMap = Object.fromEntries(k.attributes.map((a) => [a.name, a.name]));
262
+ lines.push("", `/** Props for a ${k.kind}, a custom resource of ${k.apiVersion}. */`, `export interface ${name}Props {`, ...members, "}", "", `/** A ${k.kind} (${k.apiVersion}, ${k.scope === "Cluster" ? "cluster-scoped" : "namespaced"}). */`, `export interface ${name} extends Declarable {`, ...k.attributes.map((a) => ` readonly ${a.name}: ${a.tsType};`), "}", "", `export const ${name}: new (props: ${name}Props) => ${name} = createResource(${JSON.stringify(k.resourceType)}, "k8s", ${JSON.stringify(attrMap)}) as never;`);
263
+ }
264
+ lines.push("");
265
+ return lines.join("\n");
266
+ }
267
+ function pascal(name) {
268
+ return name
269
+ .split(/[^A-Za-z0-9]+/)
270
+ .filter(Boolean)
271
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
272
+ .join("");
273
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * CRD (Custom Resource Definition) framework types.
3
+ *
4
+ * Defines the data structures used to load, parse, and process
5
+ * Kubernetes CRDs for code generation and lexicon extension.
6
+ */
7
+ export {};