Maintaining Custom Catalogs with Flux Schema CLI
A catalog is a directory tree of JSON Schemas laid out so that
flux schema validate can resolve a schema from a manifest’s apiVersion
and kind. The built-in
Flux Schema catalog ships
schemas for Kubernetes, OpenShift, Gateway API, and the Flux ecosystem;
custom catalogs let you cover internal CRDs, pin specific upstream versions,
or assemble a layout that fits your tooling.
Layout
The default resolution template is:
{{ .Group }}/{{ .Kind }}_{{ .Version }}.json
For example, apiVersion: source.toolkit.fluxcd.io/v1, kind: GitRepository
resolves to source.toolkit.fluxcd.io/GitRepository_v1.json.
All template variables are lowercased at render time, so the file names on
disk are lowercase even though apiVersion/kind are mixed case.
If you need a different layout (for example the kubeconform/kubeval flat
form), pass a full template to --schema-location:
flux schema validate ./manifests \
--schema-location './my-catalog/{{.Kind}}-{{.GroupPrefix}}-{{.Version}}.json'
Supported template variables:
| Variable | Example |
|---|---|
.Group | source.toolkit.fluxcd.io |
.GroupPrefix | source |
.Kind | gitrepository |
.Version | v1 |
An empty .Group (Kubernetes core API, e.g. apiVersion: v1) is normalized
to core, and .GroupPrefix is derived from .Group when unset. So a core
Pod renders as core/pod_v1.json with the default template and resolves to
the same path when validate looks up its schema.
The extract commands described below all share the same default template
and -f, --output-format flag, so a single --schema-location resolves
everything you generate.
Populating a catalog
The flux schema extract commands write one JSON Schema file per kind, using
the layout above by default. Mix and match these commands to cover the
Kubernetes core APIs, OpenShift resources, and any CRDs your manifests rely
on.
Kubernetes CRDs
flux schema extract crd [files...]
Reads bare CRDs, a List of CRDs, or a multi-document YAML stream, and
emits one schema file per CRD version.
| Flag | Description |
|---|---|
-d, --output-dir | Directory to write JSON Schema files to (mutually exclusive with --output-archive). |
-a, --output-archive | Path to write a gzipped tar archive of JSON Schema files to. |
-f, --output-format | Go template for output file paths (default: {{ .Group }}/{{ .Kind }}_{{ .Version }}.json). |
--strip-description | Drop description fields from the generated schemas to reduce their size. |
Generate schemas for every CRD installed in a cluster:
kubectl get crds -o yaml | flux schema extract crd -d ./my-catalog
Or from a kustomize build of an operator’s CRD bundle:
kustomize build ./config/crd | flux schema extract crd -d ./my-catalog
The output is also compatible with
kubeconformandkubeval, makingextract crda drop-in replacement for kubeconform’s openapi2jsonschema.py script.
Kubernetes APIs
flux schema extract k8s [swagger-file]
Reads a Kubernetes OpenAPI v2 swagger document and emits one file per kind
listed under x-kubernetes-group-version-kind. Helper types (e.g. PodSpec,
ObjectMeta) are inlined into the kinds that reference them, not written as
standalone files.
| Flag | Description |
|---|---|
--version X.Y.Z | Fetch the swagger from github.com/kubernetes/kubernetes for the given release tag (mutually exclusive with a swagger file). |
-d, --output-dir | Directory to write JSON Schema files to. |
-f, --output-format | Go template for output file paths (default: {{ .Group }}/{{ .Kind }}_{{ .Version }}.json). |
--strip-description | Drop description fields from the generated schemas to reduce their size. |
Pin the catalog to a specific Kubernetes release:
flux schema extract k8s --version 1.35.0 -d ./my-catalog
Or extract from a live cluster:
kubectl get --raw /openapi/v2 | flux schema extract k8s -d ./my-catalog
Core API kinds (apiVersion: v1) render under the core/ group directory.
OpenShift APIs
flux schema extract openshift [swagger-file]
Reads an openshift/api OpenAPI v2 swagger document and emits one file per
OpenShift resource. Only definitions in the openshift namespace are
emitted; embedded upstream Kubernetes types (e.g. Pod) are inlined.
| Flag | Description |
|---|---|
--ref REF | Fetch the swagger from github.com/openshift/api at the given git ref (e.g. release-4.20); mutually exclusive with a swagger file. |
-d, --output-dir | Directory to write JSON Schema files to. |
-f, --output-format | Go template for output file paths (default: {{ .Group }}/{{ .Kind }}_{{ .Version }}.json). |
--strip-description | Drop description fields from the generated schemas to reduce their size. |
Pin to an OpenShift release branch:
flux schema extract openshift --ref release-4.20 -d ./my-catalog
Or feed in a downloaded swagger:
curl -sL https://raw.githubusercontent.com/openshift/api/release-4.20/openapi/openapi.json | \
flux schema extract openshift -d ./my-catalog
A typical refresh script
A single script can populate a catalog with everything you need — the
default -f template means the outputs land side by side under the same
root and resolve to one --schema-location:
#!/usr/bin/env bash
set -euo pipefail
CATALOG=./my-catalog
mkdir -p "$CATALOG"
# Kubernetes core + built-ins
flux schema extract k8s --version 1.35.0 --strip-description -d "$CATALOG"
# Vendor CRDs at a pinned version
kubectl kustomize \
https://github.com/external-secrets/external-secrets/config/crds/bases?ref=v2.3.0 | \
flux schema extract crd --strip-description -d "$CATALOG"
Pass --strip-description to every generator to keep the catalog small —
the official catalog uses it everywhere and gets about a 54% size
reduction on native Kubernetes schemas.
The official catalog’s generator scripts under
scripts/ are a working reference: gen-k8s-schemas.sh,
gen-flux-schemas.sh, gen-crd-schemas.sh, and gen-openshift-schemas.sh.
Schema output
All extract commands emit the standalone-strict JSON Schema variant, so
schemas in the catalog are usable on their own and reproduce the
constraints the Kubernetes API server enforces:
- Every
$refis inlined, so each schema has no cross-file dependencies. - Objects with
propertiesare closed withadditionalProperties: false, except under nodes markedx-kubernetes-preserve-unknown-fields: true, which stay open so free-form maps validate correctly. - Integer-or-string fields are rewritten to
oneOf: [{type: string}, {type: integer}]. Both the legacyformat: int-or-stringand the structuralx-kubernetes-int-or-string: trueforms are recognized. - Optional fields are marked nullable (
type: [<t>, "null"]), matching the API server’s behavior of acceptingnullfor unset optional values. apiVersionandkindare injected into every kind’s properties and required list.
Hosting
A catalog is just a tree of files. The validator’s loader fetches each
rendered location over http/https or from the local filesystem.
Common options:
- Filesystem: vendor the catalog into the same repo as your manifests, or publish it as a release artifact / OCI artifact and unpack it in CI.
- HTTP(S): serve the catalog from any static host (GitHub raw, S3,
GitHub Pages, etc.) and pass the base URL to
--schema-location. The loader expands bare URLs with the default template.
A 404 or ENOENT for a given apiVersion/kind falls through to the next
--schema-location, so you can layer your catalog on top of the built-in
one:
flux schema validate ./manifests \
--schema-location ./my-catalog \
--schema-location default
Order matters — the first match wins, so put your overrides before default.
Refreshing
Custom catalogs go stale as upstream versions move. Schedule the refresh
script in CI (cron workflow, scheduled pipeline, etc.) and commit the result
back, the same way the official catalog is updated by
.github/workflows/update-catalog.yaml.