@intentius/chant-lexicon-k8s 0.124.30 → 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/apps.js +18 -0
- package/dist/actions/batch.js +6 -0
- package/dist/actions/core.js +48 -0
- package/dist/actions/index.js +13 -0
- package/dist/actions/networking.js +11 -0
- package/dist/actions/rbac.js +8 -0
- package/dist/api/classify.js +123 -0
- package/dist/api/connect.js +62 -0
- package/dist/api/fake-cluster.js +175 -0
- package/dist/api/operation-surface.d.ts.map +1 -1
- package/dist/api/operation-surface.js +109 -0
- package/dist/api/owner-chain.js +100 -0
- package/dist/api/sweep-types.js +35 -0
- package/dist/behaviour-kinds.js +82 -0
- package/dist/codegen/docs-cli.js +2 -0
- package/dist/codegen/docs.js +146 -0
- package/dist/codegen/generate-cli.js +30 -0
- package/dist/codegen/generate-lexicon.js +74 -0
- package/dist/codegen/generate-list-map-keys.js +88 -0
- package/dist/codegen/generate-operations.js +53 -0
- package/dist/codegen/generate-typescript.js +124 -0
- package/dist/codegen/generate.js +173 -0
- package/dist/codegen/naming.js +174 -0
- package/dist/codegen/package.js +36 -0
- package/dist/codegen/patches.js +88 -0
- package/dist/codegen/typecheck.js +4 -0
- package/dist/codegen/versions.js +41 -0
- package/dist/components/argo-app.js +51 -0
- package/dist/components/builders.js +22 -0
- package/dist/components/capability-plugin.js +31 -0
- package/dist/components/flux-reconcile.js +61 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +18 -0
- package/dist/components/kubectl-apply.d.ts +83 -1
- package/dist/components/kubectl-apply.d.ts.map +1 -1
- package/dist/components/kubectl-apply.js +274 -0
- package/dist/components/kustomize-apply.js +25 -0
- package/dist/composites/adot-collector.js +184 -0
- package/dist/composites/agic-ingress.js +85 -0
- package/dist/composites/aks-external-dns-agent.js +146 -0
- package/dist/composites/alb-ingress.js +86 -0
- package/dist/composites/argo-app.js +199 -0
- package/dist/composites/autoscaled-service.js +174 -0
- package/dist/composites/azure-disk-storage-class.js +47 -0
- package/dist/composites/azure-file-storage-class.js +48 -0
- package/dist/composites/azure-monitor-collector.js +176 -0
- package/dist/composites/batch-job.js +139 -0
- package/dist/composites/catalog.js +4110 -0
- package/dist/composites/cockroachdb-cluster.js +371 -0
- package/dist/composites/cockroachdb-region-stack.js +369 -0
- package/dist/composites/config-connector-context.js +40 -0
- package/dist/composites/configured-app.js +135 -0
- package/dist/composites/cron-schedule.js +38 -0
- package/dist/composites/cron-workload.js +109 -0
- package/dist/composites/ebs-storage-class.js +54 -0
- package/dist/composites/efs-storage-class.js +46 -0
- package/dist/composites/external-dns-agent.js +122 -0
- package/dist/composites/filestore-storage-class.js +49 -0
- package/dist/composites/fluent-bit-agent.js +157 -0
- package/dist/composites/flux-app.js +156 -0
- package/dist/composites/gce-ingress.js +83 -0
- package/dist/composites/gce-pd-storage-class.js +49 -0
- package/dist/composites/gke-external-dns-agent.js +129 -0
- package/dist/composites/gke-fluent-bit-agent.js +165 -0
- package/dist/composites/gke-gateway.js +91 -0
- package/dist/composites/gke-otel-collector.js +94 -0
- package/dist/composites/index.js +51 -0
- package/dist/composites/inference-service.js +76 -0
- package/dist/composites/irsa-service-account.js +77 -0
- package/dist/composites/member-defaults.js +14 -0
- package/dist/composites/metrics-server.js +181 -0
- package/dist/composites/model.js +92 -0
- package/dist/composites/monitored-service.js +152 -0
- package/dist/composites/namespace-env.js +114 -0
- package/dist/composites/network-isolated-app.js +124 -0
- package/dist/composites/node-agent.js +149 -0
- package/dist/composites/operator-stack.js +268 -0
- package/dist/composites/otel-collector-agent.js +142 -0
- package/dist/composites/otel-collector-gateway.js +216 -0
- package/dist/composites/otel-collector-node.js +176 -0
- package/dist/composites/otel-collector-rbac.js +115 -0
- package/dist/composites/otel-collector-shape.js +128 -0
- package/dist/composites/otel-collector.js +115 -0
- package/dist/composites/otel-operator-collector.js +123 -0
- package/dist/composites/ray-cluster.js +378 -0
- package/dist/composites/ray-job.js +136 -0
- package/dist/composites/ray-service.js +178 -0
- package/dist/composites/secure-ingress.js +100 -0
- package/dist/composites/security-context.js +1 -0
- package/dist/composites/sidecar-app.js +105 -0
- package/dist/composites/stateful-app.js +111 -0
- package/dist/composites/vllm-serving-runtime.js +114 -0
- package/dist/composites/web-app.js +157 -0
- package/dist/composites/worker-pool.js +167 -0
- package/dist/composites/workload-identity-sa.js +81 -0
- package/dist/composites/workload-identity-service-account.js +77 -0
- package/dist/config-schema.js +72 -0
- package/dist/config.js +59 -0
- package/dist/coverage.js +27 -0
- package/dist/crd/crd-sources.js +615 -0
- package/dist/crd/loader.js +181 -0
- package/dist/crd/oci-chart.js +144 -0
- package/dist/crd/parser.js +382 -0
- package/dist/crd/project-codegen.js +273 -0
- package/dist/crd/types.js +7 -0
- package/dist/deep-observe-hooks.js +306 -0
- package/dist/deep-observe.js +234 -0
- package/dist/default-labels.js +87 -0
- package/dist/describe-resources.js +699 -0
- package/dist/describe-stack-status.js +99 -0
- package/dist/detect.js +11 -0
- package/dist/effect-receipt-row.js +205 -0
- package/dist/export-resources.js +39 -0
- package/dist/generated/index.js +1092 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/group-namespace.js +96 -0
- package/dist/import/embedded.js +197 -0
- package/dist/import/generator.js +288 -0
- package/dist/import/live-export.js +85 -0
- package/dist/import/parser.js +204 -0
- package/dist/import/testdata/embedded/fixtures.js +60 -0
- package/dist/include/entity.js +78 -0
- package/dist/include/resolve.js +170 -0
- package/dist/index.js +59 -0
- package/dist/integrity.json +3 -3
- package/dist/kube/apply.js +90 -0
- package/dist/kube/connect.js +46 -0
- package/dist/kube/delete.js +138 -0
- package/dist/kube/describe.js +169 -0
- package/dist/kube/events.js +132 -0
- package/dist/kube/flags.js +91 -0
- package/dist/kube/get.js +159 -0
- package/dist/kube/group.js +123 -0
- package/dist/kube/logs.js +82 -0
- package/dist/kube/project.js +94 -0
- package/dist/kube/render.js +227 -0
- package/dist/kube/source.js +122 -0
- package/dist/kube/target.js +63 -0
- package/dist/kube/testing.js +28 -0
- package/dist/kube/top.js +99 -0
- package/dist/kube/verdict.js +72 -0
- package/dist/kube/wait.js +129 -0
- package/dist/kustomize/render.js +73 -0
- package/dist/kustomize/rendered-entity.js +67 -0
- package/dist/kustomize/root.js +68 -0
- package/dist/lint/audit-catalog.js +61 -0
- package/dist/lint/audit-lineage.js +118 -0
- package/dist/lint/post-synth/argo-helpers.js +36 -0
- package/dist/lint/post-synth/argo002.js +31 -0
- package/dist/lint/post-synth/argo003.js +53 -0
- package/dist/lint/post-synth/argo005.js +54 -0
- package/dist/lint/post-synth/crd-schema-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/crd-schema-helpers.js +214 -0
- package/dist/lint/post-synth/flux002.js +56 -0
- package/dist/lint/post-synth/flux003.js +50 -0
- package/dist/lint/post-synth/index.js +104 -0
- package/dist/lint/post-synth/k8s-helpers.js +186 -0
- package/dist/lint/post-synth/monitoring-selection-helpers.js +163 -0
- package/dist/lint/post-synth/otel-placement-helpers.js +293 -0
- package/dist/lint/post-synth/sops-helpers.js +43 -0
- package/dist/lint/post-synth/wk8005.js +44 -0
- package/dist/lint/post-synth/wk8006.js +56 -0
- package/dist/lint/post-synth/wk8041.js +60 -0
- package/dist/lint/post-synth/wk8042.js +36 -0
- package/dist/lint/post-synth/wk8101.js +52 -0
- package/dist/lint/post-synth/wk8102.js +30 -0
- package/dist/lint/post-synth/wk8103.js +33 -0
- package/dist/lint/post-synth/wk8104.js +58 -0
- package/dist/lint/post-synth/wk8105.js +33 -0
- package/dist/lint/post-synth/wk8201.js +44 -0
- package/dist/lint/post-synth/wk8202.js +34 -0
- package/dist/lint/post-synth/wk8203.js +34 -0
- package/dist/lint/post-synth/wk8204.js +71 -0
- package/dist/lint/post-synth/wk8205.js +38 -0
- package/dist/lint/post-synth/wk8207.js +33 -0
- package/dist/lint/post-synth/wk8208.js +33 -0
- package/dist/lint/post-synth/wk8209.js +33 -0
- package/dist/lint/post-synth/wk8301.js +49 -0
- package/dist/lint/post-synth/wk8302.js +34 -0
- package/dist/lint/post-synth/wk8303.js +68 -0
- package/dist/lint/post-synth/wk8304.js +58 -0
- package/dist/lint/post-synth/wk8305.js +96 -0
- package/dist/lint/post-synth/wk8306.js +38 -0
- package/dist/lint/post-synth/wk8401.js +86 -0
- package/dist/lint/post-synth/wk8402.js +31 -0
- package/dist/lint/post-synth/wk8403.js +49 -0
- package/dist/lint/post-synth/wk8404.js +36 -0
- package/dist/lint/post-synth/wk8405.js +65 -0
- package/dist/lint/post-synth/wk8406.js +50 -0
- package/dist/lint/post-synth/wk8407.js +58 -0
- package/dist/lint/post-synth/wk8501.js +36 -0
- package/dist/lint/post-synth/wk8502.js +35 -0
- package/dist/lint/post-synth/wk8503.js +164 -0
- package/dist/lint/post-synth/wk8504.js +20 -0
- package/dist/lint/post-synth/wk8505.js +31 -0
- package/dist/lint/post-synth/wk8601.js +40 -0
- package/dist/lint/post-synth/wk8602.js +120 -0
- package/dist/lint/post-synth/wk8603.js +63 -0
- package/dist/lint/post-synth/wk8604.js +31 -0
- package/dist/lint/post-synth/wk8605.js +124 -0
- package/dist/lint/post-synth/wk8606.js +22 -0
- package/dist/lint/post-synth/wk8701.js +31 -0
- package/dist/lint/post-synth/wk8702.js +37 -0
- package/dist/lint/post-synth/wk8703.js +31 -0
- package/dist/lint/post-synth/wk8704.js +135 -0
- package/dist/lint/post-synth/wk8705.js +38 -0
- package/dist/lint/rules/argo-appset-single-project.js +56 -0
- package/dist/lint/rules/argo-ast.js +94 -0
- package/dist/lint/rules/argo-automated-prune.js +59 -0
- package/dist/lint/rules/flux-source-ref-pin.js +44 -0
- package/dist/lint/rules/hardcoded-namespace.js +57 -0
- package/dist/lint/rules/latest-image-tag.js +103 -0
- package/dist/lint/rules/missing-resource-limits.js +85 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +57 -0
- package/dist/manifest-entity.js +71 -0
- package/dist/manifest.json +1 -1
- package/dist/micro-time.js +49 -0
- package/dist/op/activities/apply-diff.d.ts +31 -0
- package/dist/op/activities/apply-diff.d.ts.map +1 -0
- package/dist/op/activities/apply-diff.js +79 -0
- package/dist/op/activities/argo.js +98 -0
- package/dist/op/activities/ensure-secret.js +38 -0
- package/dist/op/activities/index.js +51 -0
- package/dist/op/activities/kubectl.d.ts +64 -0
- package/dist/op/activities/kubectl.d.ts.map +1 -1
- package/dist/op/activities/kubectl.js +517 -0
- package/dist/op/activities/wait-for-ready.js +229 -0
- package/dist/op/builders.js +68 -0
- package/dist/package-cli.js +21 -0
- package/dist/patch/json-patch.js +187 -0
- package/dist/patch/rendered-patch.js +94 -0
- package/dist/plugin.js +741 -0
- package/dist/project-kinds.js +56 -0
- package/dist/receipt-store.js +216 -0
- package/dist/rules/crd-schema-helpers.ts +2 -1
- package/dist/secret-labels.js +33 -0
- package/dist/secret-store.js +116 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +578 -0
- package/dist/sops/encrypted-secret-file.js +194 -0
- package/dist/sops/entity.js +32 -0
- package/dist/spec/fetch.js +58 -0
- package/dist/spec/parse.js +622 -0
- package/dist/subscribe-changes.js +168 -0
- package/dist/teardown.js +197 -0
- package/dist/telemetry-release.js +86 -0
- package/dist/validate-cli.js +17 -0
- package/dist/validate.js +47 -0
- package/dist/variables.js +34 -0
- package/dist/whoami.js +109 -0
- package/package.json +12 -12
- package/src/api/operation-surface.ts +2 -1
- package/src/codegen/package.ts +2 -2
- package/src/components/index.ts +3 -0
- package/src/components/kubectl-apply-plan.k3d.e2e.test.ts +127 -0
- package/src/components/kubectl-apply.test.ts +262 -0
- package/src/components/kubectl-apply.ts +341 -12
- package/src/deep-observe-hooks.ts +2 -2
- package/src/import/roundtrip-build.test.ts +46 -0
- package/src/lint/post-synth/crd-schema-helpers.ts +2 -1
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
- package/src/op/activities/apply-diff.ts +87 -0
- package/src/op/activities/kubectl-plan.test.ts +224 -0
- package/src/op/activities/kubectl.ts +204 -16
- 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
|
+
}
|