Schema check¶
ekn schema-check validates every rendered object the way the API server
would. It finds a typo, a wrong type or a failed policy before anything is
applied. The same validator checks each OpenTofu unit's config.tf.json; see
OpenTofu units below.
What it checks¶
JSON schema. Each object is checked against its kind's schema. Field validation is strict: an unknown field is a violation, as with
kubectl apply --validate=strict.Custom resource metadata. A CRD types
metadataonly asobject. The API server still decodes it as ObjectMeta, so the check does too.CRD CEL rules. Each
x-kubernetes-validationsrule runs withselfbound to its schema node. Schema defaults are filled first, as the API server fills them.ValidatingAdmissionPolicies. Each binding runs its policy against the rendered objects it matches:
matchConstraints,matchResources,variables,matchConditionsandvalidations.
Where the schemas come from¶
source |
used when |
|---|---|
a CRD in the render |
always; it replaces every other source for its kinds |
the cluster's |
the cluster answers |
yannh/kubernetes-json-schema |
the cluster does not answer |
|
|
A rendered CRD wins because it is what the apply installs. Server and yannh
schemas are cached under $XDG_CACHE_HOME/ekn/schemas/<cluster>, keyed by
the kube-system namespace uid.
In a live run, the check also reads the cluster's installed admission policies, their bindings and its Namespaces. A rendered object with the same kind and name replaces the installed one.
Where it runs¶
ekn schema-check: against the cluster, with the yannh fallback.--offlinereads the pinned spec and reaches no network.ekn render --validate: the offline check on the render. Violations go to stderr, and nothing is written.ekn kubeapply: the live check, after the cluster fence and before the first write.--skip-schema-checkturns it off.config.validation.schemaCheck: a Nix build of the offline check. It runs in the build sandbox, so it can gate CI.
Each run logs how long it took: load reads the schemas, check runs the
schemas and CRD rules, policies runs the admission policies.
What it cannot know¶
A render is not a request, so some CEL inputs do not exist. An expression that reads one is skipped and counted, never guessed:
requestandauthorizer.params, when a policy declares aparamKind.oldSelfin a CRD rule, andoldObjectin a policy that admits only UPDATE. An apply is evaluated as a CREATE, witholdObjectnull.namespaceObject, for a namespace that neither the render nor the cluster holds.A Kubernetes CEL extension function that cel-python does not have.
The log lists every skip by reason. An expression that fails to evaluate is a
violation, as it is on the API server, unless the policy says
failurePolicy: Ignore.
The API server also fills defaults on built-in kinds before admission, in Go
code that no schema describes. A policy expression that reads such a field
without has() can fail here and pass on the server.
OpenTofu units¶
A tf unit's config.tf.json is checked against a JSON Schema, by the same
validator as the manifests. The schema has two sources:
source |
build |
|---|---|
OpenTofu's core schema: |
opentofu-schema, the library the OpenTofu language server reads, exported by |
each provider's resources, data sources and configuration |
|
tofu.providerSchemas reads only required_providers, so a change to a
resource does not rebuild it. ekn.tofuschema merges the two into
tofu.jsonSchema and writes the rules of HCL's JSON syntax into it: a
${...} template is accepted for any value, "2" is a number, "1" is a
bool, and a block is one object or an array of them.
The check refuses an unknown argument or block, a missing required argument,
a value that does not convert to its type, a read-only attribute, a resource
or data type that no required provider declares, and a provider that
required_providers does not name.
It runs in four places:
ekn tofu plan|apply|destroy: before the firsttofu init, for every unit in the chain.--skip-schema-checkturns it off.ekn kubeapply: before the cache push and the first write, for every unit that an applied object names in itsekn.dev/tofu-unitsannotation.--skip-schema-checkturns it off.ekn deploy: in the verify stage, for everytfunit.tofu.schemaCheck: a Nix build of the same check.
An object that hands a unit to something in the cluster carries the
annotation. An operator's custom resource that names the unit's configFile
is an example. The value is a comma-separated list of unit names, and
ekn.lib.tf.unitsAnnotation holds the key:
metadata.annotations.${ekn.lib.tf.unitsAnnotation} = "day2";
kubeapply reads only the annotation, never the rest of the object. An
evaluation that names a unit which is not of class tf fails, so a typo
cannot skip the check.
nix build --file ./checks.nix tofu-schema holds the check to
tofu validate, case by case. No deploy path runs tofu validate.
Where it differs from tofu validate¶
It is stricter in three places:
An unknown argument in a
providerblock.tofu validatedoes not read the block, andtofu planrefuses it.An unknown argument in the backend.
tofu validatedoes not read it, andtofu initrefuses it.An unknown key in a nested attribute's object. OpenTofu drops the key silently, so it does nothing.
It cannot know:
A limit that a provider enforces in its own code and not in its schema. The
tlsprovider'ssubjectblock, at most one, is an example.A module call's inputs. They come from the called module, so a
moduleblock takes any argument.The type of a variable's
default.What a
${...}template evaluates to.
A backend argument the schema requires must be in config.tf.json. ekn tofu runs tofu init with no -backend-config, so nothing else can give
it, and an argument that the backend reads from the environment is refused
as missing.
opentofu-schema 0.4.3 describes OpenTofu up to 1.12. A newer tofu.package
gets the 1.12 core schema.