Skip to content

CRD-Generated Classes

The k8s lexicon generates typed classes from the core Kubernetes OpenAPI spec plus a curated list of third-party CRDs. Each CRD in the list becomes a first-class K8s::* resource — with the serializer, LSP hover, and MCP — the same as a built-in Deployment. No hand-written YAML, no as any. The constructor’s spec is an open record; its fields are checked against the CRD schema at lint time (see Spec validation).

CRD sources live in lexicons/k8s/src/crd/crd-sources.ts. At generation time (npm run generate) the CRD YAML is fetched from a pinned upstream URL, parsed, and baked into the lexicon output. The group’s first segment maps to a TypeScript namespace via the first-segment rule in crd/parser.ts — e.g. gateway.networking.k8s.ioGateway, cert-manager.ioCertManager.

group namespace example class
───────────────────────────── ─────────────── ─────────────────────────
ray.io Ray K8s::Ray::RayCluster
argoproj.io Argo K8s::Argo::Application
gateway.networking.k8s.io Gateway K8s::Gateway::HTTPRoute
crdb.cockroachlabs.com Crdb K8s::Crdb::CrdbCluster
postgresql.cnpg.io Cnpg K8s::Cnpg::Cluster
barmancloud.cnpg.io Cnpg K8s::Cnpg::ObjectStore
traefik.io Traefik K8s::Traefik::IngressRoute
cert-manager.io CertManager K8s::CertManager::Certificate
acme.cert-manager.io Acme K8s::Acme::Challenge
secrets.infisical.com Infisical K8s::Infisical::InfisicalSecret
*.toolkit.fluxcd.io Flux K8s::Flux::Kustomization
fluxcd.controlplane.io Flux K8s::Flux::FluxInstance

The Flux and CNPG rows show the override at work: six distinct groups all map to one Flux namespace (see Flux), and CNPG’s two groups both map to Cnpg, rather than each group’s first segment.

A generated CRD class takes spec: Record<string, unknown>. The field names, scalar types and enums from the CRD’s openAPIV3Schema do not reach the TypeScript constructor; they ship in the lexicon JSON instead, as a specSchema on each custom resource’s entry. Two post-synth checks read it at chant lint time:

  • WK8501 flags a spec field the CRD does not declare, with a “did you mean” suggestion. The API server would prune it and the controller would never see it.
  • WK8502 flags a scalar with the wrong type (replicas: "2") or a value outside its enum (desiredState: "Runing").

Objects the CRD marks x-kubernetes-preserve-unknown-fields or additionalProperties accept any member; x-kubernetes-int-or-string accepts either. Nothing checks a built-in kind this way, because the generated .d.ts already does.

gateway.networking.k8s.io, pinned to v1.2.1 (standard channel). The modern, portable replacement for IngressGRPCRoute in particular is the native way to express a gRPC route, instead of ingress-controller annotations.

TypeapiVersion / kind
GatewayClassgateway.networking.k8s.io/v1 / GatewayClass
Gatewaygateway.networking.k8s.io/v1 / Gateway
HTTPRoutegateway.networking.k8s.io/v1 / HTTPRoute
GRPCRoutegateway.networking.k8s.io/v1 / GRPCRoute
ReferenceGrantgateway.networking.k8s.io/v1beta1 / ReferenceGrant
Terminal window
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml
import { Gateway, HTTPRoute } from "@intentius/chant-lexicon-k8s";
export const gw = new Gateway({
metadata: { name: "edge", namespace: "infra" },
spec: {
gatewayClassName: "istio",
listeners: [{ name: "http", protocol: "HTTP", port: 80 }],
},
});
export const route = new HTTPRoute({
metadata: { name: "api", namespace: "infra" },
spec: {
parentRefs: [{ name: "edge" }],
rules: [{ backendRefs: [{ name: "api", port: 8080 }] }],
},
});

cert-manager.io + acme.cert-manager.io, pinned to v1.16.2. The de-facto controller for issuing and rotating TLS certificates. The bundle is a single multi-doc YAML.

TypeapiVersion
Certificatecert-manager.io/v1
CertificateRequestcert-manager.io/v1
Issuercert-manager.io/v1
ClusterIssuercert-manager.io/v1
Challengeacme.cert-manager.io/v1
Orderacme.cert-manager.io/v1
Terminal window
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.2/cert-manager.yaml
import { ClusterIssuer, Certificate } from "@intentius/chant-lexicon-k8s";
export const issuer = new ClusterIssuer({
metadata: { name: "letsencrypt-prod" },
spec: {
acme: {
server: "https://acme-v02.api.letsencrypt.org/directory",
email: "ops@acme.io",
privateKeySecretRef: { name: "letsencrypt-prod" },
solvers: [{ http01: { ingress: { class: "nginx" } } }],
},
},
});
export const cert = new Certificate({
metadata: { name: "api-tls", namespace: "api" },
spec: {
secretName: "api-tls",
issuerRef: { name: "letsencrypt-prod", kind: "ClusterIssuer" },
dnsNames: ["api.acme.io"],
},
});

When you set clusterIssuer on the SecureIngress composite, it now emits a genuine K8s::CertManager::Certificate (wired to the named issuer) alongside the ingress-shim annotation — no placeholder.

import { SecureIngress } from "@intentius/chant-lexicon-k8s";
export const ingress = SecureIngress("api", {
host: "api.acme.io",
serviceName: "api",
servicePort: 8080,
clusterIssuer: "letsencrypt-prod", // → annotation + a real Certificate
});

postgresql.cnpg.io, pinned to v1.29.1, plus the barman-cloud plugin’s barmancloud.cnpg.io, pinned to v0.14.0. The Postgres operator: it runs the primary/replica topology, failover, and backups.

Both groups map to the Cnpg namespace, because a Cluster and an ObjectStore are only useful together.

TypeapiVersion / kind
Clusterpostgresql.cnpg.io/v1 / Cluster
ScheduledBackuppostgresql.cnpg.io/v1 / ScheduledBackup
Backuppostgresql.cnpg.io/v1 / Backup
Poolerpostgresql.cnpg.io/v1 / Pooler
ObjectStorebarmancloud.cnpg.io/v1 / ObjectStore

The pin is 1.29.1 rather than latest because that is the operator version a real consumer runs.

Terminal window
kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.29/releases/cnpg-1.29.1.yaml
kubectl apply -f https://github.com/cloudnative-pg/plugin-barman-cloud/releases/download/v0.14.0/manifest.yaml

A cluster that archives WAL to object storage, and the nightly base backup that makes point-in-time recovery possible:

import { Cluster, ObjectStore, ScheduledBackup } from "@intentius/chant-lexicon-k8s";
export const store = new ObjectStore({
metadata: { name: "pg-backups", namespace: "app" },
spec: {
retentionPolicy: "14d",
configuration: {
destinationPath: "s3://pg-backups/barman",
s3Credentials: {
accessKeyId: { name: "backup-s3", key: "AWS_ACCESS_KEY_ID" },
secretAccessKey: { name: "backup-s3", key: "AWS_SECRET_ACCESS_KEY" },
},
},
},
});
export const db = new Cluster({
metadata: { name: "pg", namespace: "app" },
spec: {
instances: 3,
storage: { size: "10Gi" },
// Names the ObjectStore above. Nothing checks this string, so a typo
// leaves the cluster healthy with archiving pointed at nothing.
plugins: [{
name: "barman-cloud.cloudnative-pg.io",
isWALArchiver: true,
parameters: { barmanObjectName: "pg-backups" },
}],
},
});
export const nightly = new ScheduledBackup({
metadata: { name: "pg-base", namespace: "app" },
spec: {
schedule: "0 47 2 * * *", // six fields, see below
cluster: { name: "pg" },
method: "plugin",
pluginConfiguration: { name: "barman-cloud.cloudnative-pg.io" },
<Aside type="caution">
`ScheduledBackup.schedule` takes **six** fields, leading with seconds — not the five a Kubernetes `CronJob` takes. `"0 47 2 * * *"` above is 02:47 daily.
Write the five-field form out of habit`"47 2 * * *"`, meaning 02:47 to every other cron on the clusterand CNPG reads it as second 47 of minute 2 of *every hour*. The schema types the field as a plain string, so nothing rejects it. You get 24 base backups a day and no error anywhere.
</Aside>
<Aside type="note">
Adding the `plugins` block to a live Cluster injects the plugin sidecar, which **rolls the Postgres pods**. At `instances: 1` that is a brief outage.
</Aside>
## Traefik
`traefik.io`, pinned to chart **v41.1.0**. Traefik's own routing surface. An `IngressRoute` is not a `networking.k8s.io` `Ingress` with annotations — it is a separate CRD with its own matcher grammar, so before this an estate fronted by Traefik had an edge chant could not express.
| Type | apiVersion / kind |
|---|---|
| `IngressRoute` | `traefik.io/v1alpha1` / `IngressRoute` |
| `IngressRouteTCP` | `traefik.io/v1alpha1` / `IngressRouteTCP` |
| `IngressRouteUDP` | `traefik.io/v1alpha1` / `IngressRouteUDP` |
| `Middleware` | `traefik.io/v1alpha1` / `Middleware` |
| `MiddlewareTCP` | `traefik.io/v1alpha1` / `MiddlewareTCP` |
| `ServersTransport` | `traefik.io/v1alpha1` / `ServersTransport` |
| `ServersTransportTCP` | `traefik.io/v1alpha1` / `ServersTransportTCP` |
| `TLSOption` | `traefik.io/v1alpha1` / `TLSOption` |
| `TLSStore` | `traefik.io/v1alpha1` / `TLSStore` |
| `TraefikService` | `traefik.io/v1alpha1` / `TraefikService` |
Only the `traefik.io` group. The chart also ships `hub.traefik.io_*` — Traefik Hub, a different commercial product — and a vendored copy of the Gateway API, which is already generated here from its own upstream and would collide.
```bash
helm repo add traefik https://traefik.github.io/charts
helm install traefik traefik/traefik --version 41.1.0

An HTTPS route plus the HTTP one that redirects to it:

import { IngressRoute, Middleware } from "@intentius/chant-lexicon-k8s";
export const redirect = new Middleware({
metadata: { name: "redirect-https", namespace: "default" },
spec: { redirectScheme: { scheme: "https", permanent: true } },
});
export const secure = new IngressRoute({
metadata: { name: "app", namespace: "app" },
spec: {
entryPoints: ["websecure"],
routes: [{
match: "Host(`app.example.com`)",
kind: "Rule",
services: [{ name: "app", port: 80 }],
}],
tls: { secretName: "app-tls" },
},
});
export const plain = new IngressRoute({
metadata: { name: "app-http", namespace: "app" },
spec: {
entryPoints: ["web"],
routes: [{
match: "Host(`app.example.com`)",
kind: "Rule",
// The middleware is in `default`, this route is in `app`.
middlewares: [{ name: "redirect-https", namespace: "default" }],
services: [{ name: "app", port: 80 }],
}],
},
});

secrets.infisical.com, pinned to operator v0.11.7. Binds a Kubernetes Secret to an external secret provider.

TypeapiVersion / kind
InfisicalSecretsecrets.infisical.com/v1alpha1 / InfisicalSecret
InfisicalPushSecretsecrets.infisical.com/v1alpha1 / InfisicalPushSecret
InfisicalDynamicSecretsecrets.infisical.com/v1alpha1 / InfisicalDynamicSecret

The group is overridden to Infisical. The first-segment rule would give K8s::Secrets::InfisicalSecret, which reads like a core Secret with K8s::Core::Secret right there to be confused with.

Terminal window
helm repo add infisical https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/
helm install infisical-secrets-operator infisical/secrets-operator
import { InfisicalSecret } from "@intentius/chant-lexicon-k8s";
export const appSecrets = new InfisicalSecret({
metadata: { name: "app-secrets", namespace: "app" },
spec: {
hostAPI: "http://infisical.infisical.svc.cluster.local:8080",
resyncInterval: 60,
authentication: {
kubernetesAuth: {
identityId: "<machine identity>",
autoCreateServiceAccountToken: true,
serviceAccountRef: { name: "app-infisical", namespace: "app" },
secretsScope: { projectSlug: "app", envSlug: "prod", secretsPath: "/" },
},
},
managedSecretReference: {
secretName: "app-secrets", // a Deployment envFrom's this
secretNamespace: "app",
creationPolicy: "Orphan",
},
},
});

Every field there is policy: where the secret comes from, who may fetch it, what happens to the materialized Secret when the CR is deleted. None of it is a value — which is exactly why this kind is expressible in chant. Synthesis resolves nothing, so the manifest stays safe to commit. See Where Values Come From.

crdb.cockroachlabs.com, pinned to v2.17.0. The operator-managed path for a CockroachDB cluster — the operator handles version upgrades, scale-down decommissioning, and cert rotation.

TypeapiVersion / kind
CrdbClustercrdb.cockroachlabs.com/v1alpha1 / CrdbCluster
Terminal window
kubectl apply -f https://github.com/cockroachdb/cockroach-operator/releases/download/v2.17.0/install/operator.yaml
import { CrdbCluster } from "@intentius/chant-lexicon-k8s";
export const db = new CrdbCluster({
metadata: { name: "cockroachdb", namespace: "crdb" },
spec: {
dataStore: { pvc: { spec: { resources: { requests: { storage: "60Gi" } } } } },
tlsEnabled: true,
image: { name: "cockroachdb/cockroach:v24.1.0" },
nodes: 3,
},
});

The first two CRD sources. KubeRay (ray.io, v1.3.0) produces RayCluster, RayJob, and RayService. Argo CD (argoproj.io, v2.13.3) produces Application, ApplicationSet, and AppProject. Both ship value-add composites — see Argo CD Composites and the Ray + KubeRay on GKE tutorial.

The Flux GitOps Toolkit (pinned to flux2 v2.9.1) and the Flux Operator (v0.54.1). The toolkit spreads across five *.toolkit.fluxcd.io groups and the operator adds fluxcd.controlplane.io; all six collapse to a single Flux namespace, so a GitRepository and a Kustomization read as K8s::Flux::* siblings rather than scattering across Source, Kustomize, Helm, Notification, and Image.

Both sources are the release install.yaml — a multi-doc bundle of controllers, RBAC, and CRDs. The parser keeps only the CRD documents, and a kinds allowlist on each CRD_SOURCES entry narrows those to the supported set (the bundle also carries the experimental ExternalArtifact and ArtifactGenerator, left out for now).

TypeapiVersion / kind
GitRepositorysource.toolkit.fluxcd.io/v1
OCIRepositorysource.toolkit.fluxcd.io/v1
HelmRepositorysource.toolkit.fluxcd.io/v1
HelmChartsource.toolkit.fluxcd.io/v1
Bucketsource.toolkit.fluxcd.io/v1
Kustomizationkustomize.toolkit.fluxcd.io/v1
HelmReleasehelm.toolkit.fluxcd.io/v2
Providernotification.toolkit.fluxcd.io/v1beta3
Alertnotification.toolkit.fluxcd.io/v1beta3
Receivernotification.toolkit.fluxcd.io/v1
ImagePolicyimage.toolkit.fluxcd.io/v1
ImageRepositoryimage.toolkit.fluxcd.io/v1
ImageUpdateAutomationimage.toolkit.fluxcd.io/v1
FluxInstancefluxcd.controlplane.io/v1
FluxReportfluxcd.controlplane.io/v1
ResourceSetfluxcd.controlplane.io/v1
ResourceSetInputProviderfluxcd.controlplane.io/v1

Install the toolkit and, optionally, the operator:

Terminal window
# GitOps Toolkit controllers (source, kustomize, helm, notification, image)
kubectl apply -f https://github.com/fluxcd/flux2/releases/download/v2.9.1/install.yaml
# Flux Operator (manages a Flux instance declaratively)
kubectl apply -f https://github.com/controlplaneio-fluxcd/flux-operator/releases/download/v0.54.1/install.yaml
import { GitRepository, Kustomization } from "@intentius/chant-lexicon-k8s";
export const podinfo = new GitRepository({
metadata: { name: "podinfo", namespace: "flux-system" },
spec: {
interval: "1m",
url: "https://github.com/stefanprodan/podinfo",
ref: { branch: "master" },
},
});
export const apps = new Kustomization({
metadata: { name: "apps", namespace: "flux-system" },
spec: {
interval: "10m",
path: "./kustomize",
prune: true,
sourceRef: { kind: "GitRepository", name: "podinfo" },
},
});

The FluxGitSource and FluxAppFor composites collapse the GitRepository + Kustomization pair above into two calls with estate-tested defaults, and the FLUX001–003 lint rules validate the source pin, sourceRef, and dependsOn edges.

Generated CRD classes expose a read-only status accessor alongside name, namespace, and uid. It carries the resource’s server-owned runtime state — never part of the writable constructor.

import { Certificate } from "@intentius/chant-lexicon-k8s";
const cert = new Certificate({
metadata: { name: "web-tls", namespace: "prod" },
spec: { secretName: "web-tls", issuerRef: { name: "letsencrypt" } },
});
// Read-only — populated at apply time, typed from the CRD's status schema.
cert.status; // Certificate_Status — notAfter, notBefore, renewalTime, revision, conditions

The per-field status shape is generated from the CRD’s openAPIV3Schema. Scalar leaves (notAfter, revision, a RayCluster’s head.serviceIP) are typed; deeply nested or x-kubernetes-preserve-unknown-fields status degrades to an opaque record. CRDs without a status schema (config-only kinds like a Prometheus ServiceMonitor) get no status accessor.

Append an entry to CRD_SOURCES in lexicons/k8s/src/crd/crd-sources.ts and re-run codegen:

export const CRD_SOURCES: CRDSource[] = [
// ...existing entries
{ type: "url", url: "https://raw.githubusercontent.com/acme/operator/v1.0.0/config/crd/bases/acme.io_widgets.yaml" },
];
Terminal window
cd lexicons/k8s && npm run generate

Guidelines:

  • Pin the version in the URL (a vX.Y.Z tag or release asset), never main. Codegen is deterministic only if the source is.
  • One URL per CRD, or a single multi-doc bundle URL (the parser uses loadAll, as with cert-manager).
  • The group’s first segment becomes the namespace. Add a GROUP_NAMESPACE_OVERRIDES entry in crd/parser.ts if the default mapping reads poorly.
  • Document the produced classes and the operator install command in a comment block next to the entry — match the existing entries.

A kind with no generated class: k8sManifest

Section titled “A kind with no generated class: k8sManifest”

k8sManifest declares an object from its manifest, for a kind the lexicon ships no class for and you do not want to generate one for:

import { k8sManifest } from "@intentius/chant-lexicon-k8s";
export const widget = k8sManifest({
apiVersion: "acme.io/v1",
kind: "Widget",
metadata: { name: "demo", namespace: "web" },
spec: { size: 3 },
});

The props are the manifest: the serializer emits the document as written, plus the default-label and ownership merge every discovered resource gets. apiVersion and kind are required — they are what makes the object addressable — and they resolve the entity type through the same group rule a generated class uses, so acme.io/v1 Widget is K8s::Acme::Widget either way and lifecycle diff --live reads it through the same operation surface.

What you give up is the whole point of a generated class: no typed constructor, no spec validation at lint time, no LSP hover. Prefer generating the CRD. Two callers use this deliberately — a kustomize build root, whose output is already finished documents, and chant carve emit adopting a Terraform kubernetes_manifest, whose kind is only known once the body is read.