validate¶
Validate Neo4j manifests against the operator's own validators, before you apply them.
kubectl neo4j validate -f cluster.yaml
kubectl neo4j validate -f manifests/ # a directory
kubectl neo4j validate -f a.yaml -f b.yaml # repeatable
helm template . | kubectl neo4j validate -f - # stdin
Example output:
Neo4jEnterpriseCluster/prod (cluster.yaml):
✗ spec.config.dbms.default_database: Invalid value: "mydb": deprecated setting: use dbms.setDefaultDatabase() procedure instead
✗ spec.topology.serverRoles[0].serverIndex: Invalid value: 7: must be in range [0, 1]
⚠ Even number of servers (2) may reduce fault tolerance when databases specify odd-numbered server allocations. Consider using an odd number of servers for optimal fault tolerance.
⚠ 2 servers provide limited fault tolerance. If one server fails, databases may lose quorum. Consider using 3 or more servers for production deployments.
1 validated, 0 skipped — 2 error(s), 2 warning(s)
validated against operator rules v1.17.0
Errors are listed before warnings, each sorted by field path.
Flags¶
| Flag | Meaning |
|---|---|
-f |
File, directory, or - for stdin. Repeatable. A directory is read one level deep (.yaml, .yml, .json), not recursively. |
--connect |
Connect to the current kubeconfig context to check cross-references. |
--context |
Kubeconfig context to use (implies --connect). |
--kubeconfig |
Path to the kubeconfig file (implies --connect). |
--namespace, -n |
Namespace for manifests that omit one. With --connect, it defaults to the kubeconfig context's namespace, else default — where kubectl apply would put them. |
--strict |
Treat warnings as errors (exit non-zero on warnings). Pending is unaffected. |
--quiet |
Print findings only — no per-file "ok" lines, no summary. |
Exit codes¶
These are a stable contract, so you can rely on them in CI:
| Code | Meaning |
|---|---|
0 |
No errors. Warnings alone do not fail unless --strict. |
1 |
At least one validation error (or a warning under --strict). |
2 |
Usage problem: bad flags, unreadable file, undecodable YAML, or (with --connect) a cluster that could not be reached. |
What it checks — and what it doesn't¶
Misspelled fields¶
A key the kind does not have is reported with its full path, one line per field:
$ kubectl neo4j validate -f standalone.yaml
Neo4jEnterpriseStandalone/db (standalone.yaml):
✗ spec.auth.secretRef: Invalid value: unknown field: this kind has no such
field. Check the spelling against the API reference — the operator ignores
it and the API server rejects the manifest.
This is the most common manifest error there is.
The check descends into lists of objects as well, and reports the index of the offending item — spec.topology.serverRoles[0].modeContraint, spec.env[1].valueFrom.secretKeyRef.kee — so a typo in serverRoles, env, a composite's constituents or any other list is caught the same way as one in a plain object. Lists of plain values ([]string, []int) have no field names to misspell and are left alone.
Open maps are left alone, because every key in them is your data rather than a field name: spec.config, a composite constituent's driverSettings, labels and annotations. metadata and status are not walked either — they belong to Kubernetes, not to this operator.
This check is offline, so it runs on every one of the 27 kinds — including the ones whose cross-reference rules need --connect, and the ones with no operator-side validator at all. "No validator" is not "no spelling": the kinds governed only by their CRD schema can still be misspelled, and the API server is a slower place to find out. Such a document reports the typo and says what is still unchecked:
Neo4jShardedDatabase/products-sharded (sharded.yaml):
✗ spec.propertyShards: Invalid value: unknown field: …
- not fully checked — Neo4jShardedDatabase validation resolves cross-references;
re-run with --connect to check it
It is not a replacement for --dry-run=server¶
kubectl apply --dry-run=server already enforces the CRD schema: field types, enums, numeric ranges, required fields, and the CEL immutability rules. validate does not duplicate that.
What it adds is everything that lives in the operator's validators beyond the schema — the rules the API server has no way to know about:
- cross-field topology rules,
serverRolesindex bounds and duplicate detection spec.configkeys the operator rejects: deprecated 4.x settings, and keys the operator manages itself at runtime (advertised addresses, discovery, topology)- TLS strictness and issuer-capability rules
- storage, image, and licence-acceptance requirements
- advisory topology warnings — even server counts, two-server clusters with limited fault tolerance, all-PRIMARY or all-SECONDARY mode constraints
The two are complementary. In CI, run both.
What gets checked, and what doesn't¶
The operator has 27 CRD kinds, but only 13 have operator-side validators. The command reports three distinct outcomes, and the distinction matters:
| Kinds | Behaviour | |
|---|---|---|
| Checked offline | Neo4jEnterpriseCluster, Neo4jEnterpriseStandalone, Neo4jBackup, Neo4jPlugin, Neo4jDatabaseAlias, Neo4jCompositeDatabase, Neo4jReplicaDatabase |
Validated with no cluster |
Need --connect |
Neo4jDatabase, Neo4jUser, Neo4jRole, Neo4jRoleBinding, Neo4jAuthRule, Neo4jShardedDatabase |
Skipped offline; validated when connected |
| No validator at all | the 12 Aura kinds, Neo4jRestore, Neo4jReplicaPromotion |
Only the CRD schema applies — use kubectl apply --dry-run=server |
- Neo4jUser/analytics (users.yaml): skipped — Neo4jUser validation resolves cross-references; re-run with --connect to check it
- Neo4jRestore/restore-1 (restore.yaml): skipped — no operator-side validator for Neo4jRestore; its CRD schema still applies (kubectl apply --dry-run=server)
Skipped is never a failure. For the middle row, reporting "cluster not found" for a cluster that is merely unreachable would be a false error — worse than not checking — so the command declines to guess. For the bottom row, no amount of connecting will help, and the message says so rather than sending you after a check that does not exist.
Checking cross-references with --connect¶
kubectl neo4j validate -f manifests/ --connect # current kubeconfig context
kubectl neo4j validate -f manifests/ --context prod # a specific context
kubectl neo4j validate -f manifests/ --connect -n team-a # namespace for manifests that omit one
Connecting is opt-in. Defaulting to the current context would make a command documented as offline fail in CI, where there is usually no kubeconfig at all.
With a connection, the command resolves clusterRefs, password Secrets and role references, and adds a third finding type:
Neo4jUser/analytics (users.yaml):
… waiting for password Secret "analytics-password" in namespace "neo4j"
1 validated, 0 skipped — 0 error(s), 0 warning(s), 1 pending
Pending is neither an error nor a warning. It is the operator's own category for a dependency that is not satisfiable yet — a Secret you have not applied — as opposed to something wrong. Pending never affects the exit code, including under --strict: failing a pipeline because a Secret has not been created yet would punish a correct manifest.
The connection is also what makes version-skew detection possible. When connected, the CLI compares itself against the operator running in the cluster:
⚠ version skew: this CLI carries v1.17.0 rules, but the operator in neo4j-operator/neo4j-operator-controller-manager is v1.16.0.
Rules added or removed between those releases are checked incorrectly here.
It is advisory and silent when it cannot tell — the operator may be in a namespace you cannot list, which is not worth failing over.
Required permissions¶
--connect reads only. It needs get on Neo4jEnterpriseCluster, Neo4jEnterpriseStandalone, Neo4jRole and Secret in the namespaces you are validating (plus list on Neo4jUser when validating a Neo4jRoleBinding, and get on Neo4jShardedDatabase when validating a Neo4jRole), plus list on Deployment for the version check (which degrades silently without it). It never writes anything.
A clean run may not be the last word¶
The operator stops validating a resource after certain critical errors — an invalid image, for example — because later checks are not meaningful once the image is wrong. validate reproduces that faithfully rather than inventing its own behaviour, so:
Fixing everything reported and re-running may surface further errors.
A clean run means nothing further is reachable, not that nothing was ever wrong. Re-run until two consecutive runs agree.
Version skew¶
The output always names the ruleset it used:
The CLI carries the validation rules of the release it was built from. If your cluster runs a different operator version, a rule added later will not be checked and a rule since removed may still be enforced. Keep the CLI on the same version as the operator you deploy.
In CI¶
Because it needs no cluster, it fits any pipeline that can run a binary:
- name: Validate Neo4j manifests
run: |
curl -sSL "https://github.com/priyolahiri/neo4j-kubernetes-operator/releases/download/v${VERSION}/kubectl-neo4j_${VERSION}_linux_amd64.tar.gz" | tar -xz
./kubectl-neo4j validate -f manifests/ --strict
Or as a pre-commit hook:
- repo: local
hooks:
- id: neo4j-validate
name: Validate Neo4j manifests
entry: kubectl-neo4j validate -f manifests/
language: system
files: '^manifests/.*\.ya?ml$'
pass_filenames: false
pass_filenames: false matters: validate reads only the paths given with -f, and silently ignores any extra positional arguments, so a hook that let pre-commit append the staged filenames would validate just the first one.
--strict is the usual choice for CI: it makes advisory warnings — such as a two-server cluster that will lose quorum if one server fails — block the pipeline rather than scroll past.
See also¶
- Resource Sizing — the floors behind several of the warnings
explain— for a resource that applied cleanly but is not becoming Ready