Skip to content
Kubernetes RenderingManifest validation

Manifest validation

Nyl runs configured validators against final Kubernetes manifests. render and render-tree emit their output even when validation fails so it can be inspected; they return a failing exit status. render-tree --check writes no output. Validation must succeed before calculating diffs, applying resources, or publishing a target. The CI image includes the pinned kubeconform executable. For local development, install the tools in the project mise configuration.

Add a validator to nyl.toml:

[validation]
enabled = false
[validation.kubeconform]
strict = true
builtin_schemas = "cached"
schema_locations = []
skip = []
timeout_seconds = 60

The kubeconform table selects that validator. enabled = true runs configured validators automatically for render, diff, apply, render-tree, diff-tree, and publish-tree. --validate enables them for one invocation; --no-validate suppresses automatic validation. Requesting validation without a configured validator is an error.

strict = true rejects unknown fields as well as duplicate YAML keys. Captured CRDs are vendored whole, and both strict and permissive schema variants are derived from them, so this setting can change without recapturing. Both variants validate declared constraints; permissive mode allows extra fields where the schema does not explicitly constrain them. Strict mode respects fields that explicitly preserve unknown properties.

Terminal window
nyl render-tree --target production --output-dir rendered --validate
nyl diff-tree --target production --validate
nyl publish-tree --target production --validate
nyl render app.yaml --kube-version 1.31.4 --validate

Target-aware validation uses the Cluster’s effective Kubernetes version. Targetless validation requires --kube-version; Helm rendering also requires --kube-api-versions. The schema lookup normalizes a leading v, distribution suffixes, and a missing patch version, without falling back to another version.

Validation runs on cache hits, render-tree --check, and publish-tree --dry-run. A filtered diff-tree validates the entire desired tree, then selects the differences to display. Its baseline is allowed to contain invalid resources so that corrections remain diffable.

apply validates the submitted set before writing resources or release state. In append mode, diff validates its composed desired set, while apply validates the resources submitted in that invocation.

Diagnostics and summaries go to stderr, preserving manifest and diff stdout. Each destination reports schema preparation, the number of resources submitted to kubeconform, and its elapsed validation time. A final summary counts resource outcomes across destinations. Borrowed CRD snapshots identify their source Cluster. Failures are reported when that destination’s validation completes. Invalid resources, missing schemas, tool failures, and timeouts fail the command. Explicit skip entries use apiVersion/kind, such as example.com/v1/Widget; the summary reports skipped resources. Lists are expanded for schema discovery and validation, with item paths retained in diagnostics. A skipped List excludes its entire contents. Schema validation does not execute CEL, admission webhooks, or conversion webhooks, and does not establish deployment ordering or compatibility with resources outside the validated input.

Cluster capture explicitly reads live capabilities and CRDs. Its default policy is equivalent to:

[capture.cluster]
crds = true
Terminal window
nyl capture cluster staging
nyl capture cluster staging --crds --context staging-admin
nyl capture cluster staging --check
nyl capture cluster staging --no-crds

The command refreshes committed Kubernetes capabilities and, when enabled, schemas for every served CRD version. --check compares the enabled capture outputs with the live cluster without writing. --no-crds refreshes capabilities while preserving schema files; it does not certify those schemas as current. Capturing schemas is enabled by default, including when nyl init invokes capture, and requires permission to list CRDs. Set [capture.cluster] crds = false to default to capabilities-only capture; --crds overrides that setting. A CRD read failure aborts capture before writing. nyl vendor consumes committed captures and does not contact clusters.

A capture with CRDs replaces that Cluster’s schema inventory with the served CRD versions currently in the cluster, removing entries for deleted CRDs and versions that are no longer served. Unreferenced schema blobs remain on disk until nyl vendor --prune; blobs referenced by another Cluster are retained.

Snapshots use vendor.path, or vendor/ when that setting is absent:

vendor/
clusters/staging/schemas.json
schemas/blobs/<sha256>.json

The cluster inventory lists each CRD’s group, kind, scope, and served versions, and references its vendored definition blob. Equal definitions are stored once across clusters. Volatile resource metadata, status, and conversion webhook CA bundles are excluded, so a definition changes only with its API contract. Inventories written by earlier Nyl versions are not read; recapture them with nyl capture cluster <name> --crds and remove their old blobs with nyl vendor --prune. Commit the inventories and blobs with the Cluster configuration. Capture and vendoring generate schemas/.gitattributes to treat blobs as binary in Git, suppressing text diffs and merges and preserving the exact bytes required by their hashes. Blobs remain JSON in ordinary Git; inventories remain diffable.

With CRDs vendored, the Cluster’s spec.kubernetes records only the APIs and cluster-scoped kinds the CRDs do not serve and sets vendoredCrds: true. Rendering and validation add the CRD-served API versions and scopes back from the snapshot.

A snapshot includes its source capabilities fingerprint. Validation rejects a mismatch and instructs recapture, including when a capture was interrupted between updating the schema inventory and Cluster source.

A production Cluster can use a committed staging contract when production is inaccessible to the rendering pipeline:

apiVersion: k8s.nyl/v1
kind: Cluster
metadata:
name: production
spec:
destination:
server: https://production.example.com
apiContractFrom:
clusterRef:
name: staging
mode: all

all inherits Kubernetes version, API capabilities, and the CRD schema source. Omit spec.kubernetes in this mode. schemas borrows only the CRD schema source and requires production’s own spec.kubernetes block. Rendering and validation use the same effective capabilities.

Destinations, values, labels, and live connection settings remain local. References may form acyclic chains. nyl get clusters reports effective versions and borrowed provenance. Capture the source Cluster; capture of an inheriting Cluster is rejected with the source name.

Borrowing asserts API compatibility; it is not an observation of production. Validation reports the destination and the source of its captured schemas. Generated Argo CD Applications and AppProjects use their ArgoCDInstance Cluster’s contract, independently of the workload destination.

Validate an operator installation or upgrade

Section titled “Validate an operator installation or upgrade”

Complete target-tree validation uses desired CRDs automatically. This applies to render-tree, diff-tree, and publish-tree whenever validation is enabled:

Terminal window
nyl publish-tree --target production --validate

Use --no-use-desired-crds to validate against local or captured CRD schemas. This switch does not enable validation. File-based render, diff, and apply require --use-desired-crds to enable validation using desired CRDs; it cannot be combined with source/output kind filters or --append-release.

Schemas are resolved in this order, independently for each destination:

  1. Desired CRDs, when enabled for the invocation.
  2. Configured local schema locations.
  3. The effective Cluster’s captured CRD schemas.
  4. The pinned registry for built-in Kubernetes APIs.

A desired CRD supersedes its entire group/kind definition. Removed and unserved versions cannot fall back to captured schemas. Conflicting desired definitions fail. A group/kind absent from desired CRDs retains ordinary schema resolution.

Validation checks compatibility of resources included in the render. It does not check resources in other targets or existing cluster resources omitted from the input, establish upgrade ordering, or perform stored-version migration. Tree diff selection filters do not narrow the complete desired validation input.

For APIs managed elsewhere, commit schemas locally and configure a lookup:

[validation.kubeconform]
schema_locations = [
'schemas/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json',
]

Lookup templates support kubeconform’s Group, ResourceKind (lowercase), ResourceAPIVersion, KindSuffix, NormalizedKubernetesVersion, and StrictSuffix fields. Paths and relative schema references must stay inside the project and must not traverse symlinks. A location without a JSON extension uses the standard Kubernetes schema directory layout. External HTTP references in local schemas are not admitted; commit their dependencies locally.

Built-in schemas come from an immutable commit of yannh/kubernetes-json-schema. The effective Kubernetes version selects the version directory within that registry snapshot. The kubeconform executable version and registry revision are independent pins. A project can set builtin_schema_revision to a full commit SHA when it needs a different registry snapshot.

CustomResourceDefinition schemas use the registry’s shared definitions because their recursive schemas are not available as standalone files. Their complete reference graph uses the selected Kubernetes version and strictness policy.

By default, built-ins are downloaded into a disposable cache. To require committed schemas during validation:

[vendor]
mode = "required"
[validation.kubeconform]
builtin_schemas = "vendor-used"
Terminal window
nyl vendor
nyl vendor --check
nyl diff-tree --target production --validate
nyl publish-tree --target production --validate

builtin_schemas selects one of three modes:

ModeVendoringValidation
cached (default)No builtin schema populationDownload into a disposable cache as needed
vendor-usedSchemas required by selected target rendersRequire vendored schemas
vendor-allEvery JSON schema in each selected Kubernetes version directoryRequire vendored schemas

Both vendor modes include catalog destinations and schema dependencies. vendor-all uses the configured strictness and immutable registry revision, regardless of local schema overrides or resource exclusions. It records complete directory inventories so nyl vendor --check can verify coverage offline. Adding a builtin resource kind for those versions requires no new vendor run. Changing Kubernetes version, strictness, or registry revision requires vendoring its matching schemas. In both vendor modes validation fails on missing vendored schemas and never downloads a fallback. A targetless render must also have all of its required built-ins available locally.

nyl vendor repairs missing or corrupt builtin schema blobs from the cache or pinned registry. nyl vendor --refresh fetches required builtins and their dependencies again; validation and --check reject damaged blobs without repair.

nyl vendor does not access clusters or refresh captures. --prune preserves blobs referenced by every captured Cluster, including inherited sources, and aborts schema pruning if an inventory is malformed.

Publication loads validation policy, inheritance references, and schemas from the source snapshot selected for the published artifact. Commit these inputs before publishing; invocation flags apply to that selected snapshot.

Human findings group field errors beneath each resource and include its rendered location and recorded Nyl authoring provenance. The expansion trace identifies source documents, Components, HelmCharts, remote repository revisions, and Nyl operations when available. It is a resource-level trace, not a field-to-template source map. Schema instance paths come from kubeconform; source line numbers are not inferred from them.

Each failure leads with the validated resource and its field errors. Rendered: locates the output document, Source: locates the authoring document, and successive Expanded from: rows identify the resources in its expansion trace. Document suffixes and provenance API versions share a dimmed, aligned column within each resource’s details. Terminal and CI findings use a bold cyan header and red failure labels, following Nyl’s --color policy and NO_COLOR. Text and JSON exports remain unstyled.

Export the same results independently of manifest output:

Terminal window
nyl render-tree --output-dir tmp \
--validation-output json:validation.json \
--validation-output text:validation.txt
nyl render-tree --check --output-dir tmp \
--validation-output json:- --no-validation-stderr

--validation-output FORMAT:PATH is repeatable and supports json and text. An export request enables configured validation and conflicts with --no-validate. render, render-tree, diff, diff-tree, apply, and publish-tree support file exports. Only render-tree supports PATH=-; its informational messages then go to stderr so stdout contains exactly one report. Plain render reserves stdout for manifests.

--no-validation-stderr suppresses human findings and the final validation summary. Progress and unexpected operational errors remain visible. Exported reports retain their findings regardless of this switch.

Reports are written atomically on successful or failing validation. Keep them outside managed render trees, vendor directories, and source files. Duplicate or conflicting output destinations are rejected. A report write failure fails the command and blocks diff, apply, and publication. Rendering still attempts to produce inspectable manifests.

The envelope contains version, status, complete, summary, destinations, resources, and operationErrors. JSON contains every resource result, including valid and skipped resources. Ordering is deterministic by destination, rendered path, document number, List item indexes, resource identity, and finding path/message. Reports contain no ANSI formatting or elapsed-time fields.

Each resource result contains:

  • validator and destination.
  • resource: apiVersion, kind, nullable namespace, and nullable name.
  • renderedLocation: target-relative path, one-based document, and items containing zero-based indexes for nested Lists. File-based operations identify the validated stream as <rendered>; authoring paths are in provenance.
  • status: valid, invalid, error, skipped, or notChecked.
  • findings: a list of nullable validator instance path and human-readable message pairs. An empty path identifies the resource root; null means the validator supplied no field location.
  • provenance: ordered frames tagged by type. source has path and document; resource has identity; generated has operation; remote has repository and resolved revision. An empty list means unavailable.
  • schemaOrigin: null when unresolved, or a tagged origin. captured contains cluster and digest; desired contains crd and digest; local contains path and digest; builtin contains url and nullable digest. Digests are SHA-256 over the schema’s RFC 8785 canonical JSON; built-in URLs retain the registry commit and selected Kubernetes version.

summary counts resources as valid, invalid, errors, skipped, and notChecked. Multiple findings on one resource count as one invalid resource. Operation errors have an optional destination and a message; they are separate from resource error counts.

The overall status is valid, invalid, or error. A resource error or incomplete operation produces error; otherwise any invalid resource produces invalid. complete means every input resource has a trustworthy outcome and there are no operation errors. Schema preparation failures, malformed tool responses, and timeouts stop validation, preserve completed destinations, and leave unchecked resources as notChecked. Ordinary invalid resources do not stop later destinations. Skipped Lists appear as one skipped resource with their contents excluded.

Failures before a validation input inventory can be constructed remain ordinary command errors and do not produce a validation report. Exporting findings does not change the validation exit status or authorize deployment.