Add a Third-Party CRD
Most operators ship their API as Kubernetes CustomResourceDefinitions. Because a CRD carries an openAPIV3Schema, the k8s lexicon can generate typed resources from it the same way it generates the built-in Kubernetes types. You do not write a new lexicon for KubeRay, Argo CD, cert-manager, or Crossplane — you add their CRDs to the k8s lexicon and, optionally, author rules against the kinds they introduce.
This is the path KubeRay, Argo CD, Gateway API, the CockroachDB operator, cert-manager, and the Prometheus operator all took. This page documents it.
The CRD source list
Section titled “The CRD source list”Generation reads its third-party CRDs from one array, CRD_SOURCES in lexicons/k8s/src/crd/crd-sources.ts. Each entry names where a CRD document is fetched from at generation time:
export const CRD_SOURCES: CRDSource[] = [ // KubeRay — ray.io/v1 { type: "url", url: `${KUBERAY_CRD_BASE}/ray.io_rayclusters.yaml` }, { type: "url", url: `${KUBERAY_CRD_BASE}/ray.io_rayjobs.yaml` }, { type: "url", url: `${KUBERAY_CRD_BASE}/ray.io_rayservices.yaml` }, // ...cert-manager, Argo, Gateway API, Prometheus, Cockroach];A CRDSource is one of three shapes:
| Shape | Fields | Use |
|---|---|---|
{ type: "url", url } | a raw CRD YAML URL | the default — pin an operator release |
{ type: "file", path } | a local path | a CRD vendored into the repo |
{ type: "cluster", context?, namespace? } | a kubectl context | read CRDs already installed in a cluster |
Pin the operator version in a const next to the entry (see the KUBERAY_VERSION / ARGOCD_VERSION constants at the top of crd-sources.ts) so an upgrade is a one-line change and the source URL always resolves to a known revision.
The CRD YAML is fetched and baked in at generation time, not at build time.
chant buildstays offline and deterministic.
Generate
Section titled “Generate”From the k8s lexicon package, run generation. It fetches each source, parses the schema, and emits the typed surface:
npm run generate -w @intentius/chant-lexicon-k8sGeneration writes three artifacts into src/generated/: index.d.ts (the types), index.ts (the runtime factory index), and lexicon-k8s.json (the registry). The log line reports the counts and any CRD it failed to load.
What a CRD becomes
Section titled “What a CRD becomes”Each CRD kind becomes a resource type K8s::<Namespace>::<Kind>. The namespace comes from the CRD’s API group: the first segment of the group is PascalCased, with a small override table for groups whose first segment reads badly.
| CRD group | Namespace | Example type |
|---|---|---|
ray.io | Ray | K8s::Ray::RayCluster |
cert-manager.io | CertManager | K8s::CertManager::Certificate |
monitoring.coreos.com | Monitoring | K8s::Monitoring::ServiceMonitor |
argoproj.io | Argo (override) | K8s::Argo::Application |
The mapping lives in
normalizeGroupName/GROUP_NAMESPACE_OVERRIDESinlexicons/k8s/src/crd/parser.ts. Add an override there if a group’s first segment collides or reads poorly.
The generator types the writable spec. It deliberately drops apiVersion, kind, and status (see extractProperties in crd/parser.ts) — status is runtime state, not authoring input.
Verify
Section titled “Verify”Confirm the kind you added is present and the package still builds:
grep -R "RayCluster" src/generated/index.d.ts # the type existsnpm run build -w @intentius/chant-lexicon-k8s # the lexicon compilesThen import it in a scratch file and check that editor completion resolves the spec — that is the fastest confirmation the schema round-tripped.
Scale — do not fetch a whole provider
Section titled “Scale — do not fetch a whole provider”A CRD source is one CRD document. Some operators ship hundreds — a full Crossplane provider-aws is on the order of a thousand kinds. Generating all of them at once produces an enormous types file and slow type-checking (the same failure mode the Azure codegen hit). Add the specific kinds a project uses, or scope by resource group. Never point a source at an operator’s entire CRD bundle without a reason.
Add rules for the new kinds
Section titled “Add rules for the new kinds”Types give you structural validation for free — a misspelled field or a wrong type will not compile. Everything semantic is a rule you author against the kind by name. The kind being CRD-derived changes nothing; rules target it exactly as they target a built-in resource.
Argo CD is the worked precedent. It arrives through CRD_SOURCES and carries a full rule set:
- Pre-synth (source AST) —
ARGO001inlint/rules/argo-automated-prune.tsflags a productionApplicationthat enablessyncPolicy.automated.prune. Wire a pre-synth rule by importing it into thelintRules()array inlexicons/k8s/src/plugin.ts. - Post-synth (emitted manifests) —
ARGO003inlint/post-synth/argo003.tschecks eachApplication.spec.destinationagainst the cluster Secrets in the same output. Drop a post-synth check file inlint/post-synth/and it is discovered automatically; no manual registration.
The post-synth cross-resource pattern — collect every manifest of kind A, validate each kind B against them — is the one worth studying, because XR-against-XRD conformance and Composition-references-a-real-kind checks are the same shape. See Post-Synth Checks for that pattern in full.
Next steps
Section titled “Next steps”- Write Lint Rules — pre-synth imperative and declarative rules
- Post-Synth Checks — validate the serialized bundle, including cross-resource checks