Add a Third-Party CRD
Show the whole prompt
Read https://intentius.io/chant/lexicon-authoring/crd-sources/.
Add the CRDs of the operator I name to `CRD_SOURCES` in `lexicons/k8s/src/crd/crd-sources.ts`, pinned to a release in a version const and kept to the kinds a project uses, with the doc comment the other entries carry.
Run `npm run generate -w @intentius/chant-lexicon-k8s` and `npm run build -w @intentius/chant-lexicon-k8s`, check that each kind appears in `lexicons/k8s/src/generated/index.d.ts`, and open a pull request with the result.
Do not point a source at a whole provider bundle, and do not set `CHANT_K8S_ALLOW_CRD_FAILURES`.
Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.Optional: hand this page to your coding agent
Read https://intentius.io/chant/lexicon-authoring/crd-sources/.Add the CRDs of the operator I name to `CRD_SOURCES` in `lexicons/k8s/src/crd/crd-sources.ts`, pinned to a release in a version const and kept to the kinds a project uses, with the doc comment the other entries carry.Run `npm run generate -w @intentius/chant-lexicon-k8s` and `npm run build -w @intentius/chant-lexicon-k8s`, check that each kind appears in `lexicons/k8s/src/generated/index.d.ts`, and open a pull request with the result.Do not point a source at a whole provider bundle, and do not set `CHANT_K8S_ALLOW_CRD_FAILURES`.Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.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. For KubeRay, Argo CD, cert-manager or Crossplane, add their CRDs to the k8s lexicon and, optionally, author rules against the kinds they introduce.
Every third-party API in the k8s lexicon arrived this way, from KubeRay and Argo CD to the ACK controllers. Read CRD_SOURCES for what is there today. This page shows how to add to 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[] = [ { 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, Flux, Gateway API, Prometheus, Cockroach, ACK];The CRDSource type is in lexicons/k8s/src/crd/types.ts; loadCRDs in
lexicons/k8s/src/crd/loader.ts fetches each one. Each block in
crd-sources.ts is preceded by a doc comment naming the operator, its API
group, the kinds it produces and the install command for its controller. Follow
that shape when you add one.
A CRDSource is one of four shapes:
| Shape | Fields | Use |
|---|---|---|
{ type: "url", url } | a raw CRD YAML URL | the default — pin an operator release |
{ type: "helm", chart, version, digest?, chartSubdir? } | a chart reference | an operator that ships CRDs only in its chart |
{ 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 |
Every shape also takes an optional kinds allowlist, which keeps a multi-document bundle down to the kinds you want.
Pin the operator version in a const next to the entry (see the KUBERAY_VERSION and 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.
When the CRDs are only in a chart
Section titled “When the CRDs are only in a chart”Operators built with the Java and Go operator SDKs generate their CRDs at build time from the controller’s types, so there is often nothing to link to. KubeMicroVM commits one of its five CRDs and publishes all five in its chart.
type: "helm" pulls and unpacks the chart at generation time and reads every YAML document out of its crds/ directory, which is where Helm’s own convention puts them:
{ type: "helm", chart: "oci://ghcr.io/codriverlabs/helm/kube-microvm-operator", version: "1.0.11", digest: "sha256:b15570736277f69bc69bc6bd1d6b89470404b8bbd45970b7dcb29ca9ab69170c", kinds: ["MicroVM", "MicroVMImage", "MicroVMNetwork", "MicroVMClass", "MicroVMReplicaSet"],}version is required, because an unpinned chart makes generated output depend on the day it was generated. chartSubdir overrides crds for charts that keep them somewhere else. The kinds allowlist works as it does for a multi-doc URL bundle.
An oci:// chart is pulled over the registry’s HTTP API with an anonymous token and unpacked in process, so generation does not need helm on PATH and produces the same surface on a fresh CI runner as on a workstation. digest pins the chart’s content layer (the sha256: the registry manifest lists for the application/vnd.cncf.helm.chart.content.v1.tar+gzip layer); with it, a re-pushed tag fails generation instead of changing the output. A classic https:// chart repository still needs the helm binary. Prefer url when the operator publishes its CRDs, since it is one fewer network shape.
The CRD YAML is fetched and baked in at generation time, not at build time.
chant buildstays offline and deterministic. See Network Egress for the guard that keeps that true.
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. A source that fails to load (a registry that is down, a digest that no longer matches) fails generation, and the error names every such source, so a missing operator cannot drop its kinds from the output unnoticed. To generate without them on purpose, for example offline, set CHANT_K8S_ALLOW_CRD_FAILURES=1: each failure is then a warning and the output lacks that source’s kinds.
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 |
lambda.aws.amazon.com | KubeMicroVM (override) | K8s::KubeMicroVM::MicroVM |
secrets.infisical.com | Infisical (override) | K8s::Infisical::InfisicalSecret |
postgresql.cnpg.io | Cnpg (override) | K8s::Cnpg::Cluster |
barmancloud.cnpg.io | Cnpg (override) | K8s::Cnpg::ObjectStore |
An override earns its place for one of two reasons. Either the first segment reads badly on its own (secrets.infisical.com would give K8s::Secrets::InfisicalSecret, which looks like a core Secret next to K8s::Core::Secret), or several groups belong together. The last two rows are the second case: a CNPG Cluster names a barman-cloud ObjectStore in its plugins[] block, so splitting them into Postgresql and Barmancloud would scatter two kinds that are only ever used as a pair. Flux does the same with six groups.
The generator types the writable spec and metadata. extractProperties in crd/parser.ts skips apiVersion and kind (the serializer supplies both) and skips status, which extractStatusType re-attaches as a read-only status attribute instead of an authoring property.
Verify
Section titled “Verify”Confirm the kind you added is present and the package still builds:
grep -R "RayCluster" lexicons/k8s/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: a misspelled field or a wrong type will not compile. Everything semantic is a rule you author against the kind by name. Rules target a CRD-derived kind 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.
The post-synth cross-resource pattern (collect every manifest of kind A, validate each kind B against them) is the one to study, 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