Skip to content

preflight

Checks the cluster-side preconditions a manifest depends on — the things validate cannot see because they are not in the manifest.

kubectl neo4j preflight -f cluster.yaml -n neo4j       # before you apply
kubectl neo4j preflight Neo4jBackup/nightly -n neo4j   # for what is deployed
kubectl neo4j preflight -n neo4j                       # everything there

validate took the operator's spec rules and moved them from after-apply to before-apply. But a large class of first failures is not in the manifest at all — it is in the cluster the manifest is about to land in. A StorageClass that does not exist. One that cannot expand. A credentials Secret missing a key the backup Job will mount. No node large enough for the pod. Each produces a correct-looking manifest that never becomes a running database.

$ kubectl neo4j preflight -f cluster.yaml -n neo4j
Neo4jEnterpriseCluster/prod (cluster.yaml)
  ✗ storageclass fast-ssd — does not exist
      → The operator will refuse to create the StatefulSet and record a
        StorageClassNotFound event. Create the class, name an existing one, or leave
        spec.storage.className empty to use the cluster default.
  ✗ node capacity — no Ready node can fit a 2Gi memory request
      largest Ready node has 1Gi allocatable
      → Every pod will stay Pending as Unschedulable. Add capacity, or lower
        spec.resources.requests.memory — but Neo4j Enterprise will not start below
        1.5Gi, so that has a floor.

Shape only: no bucket, registry or endpoint was contacted.
1 of 1 resource(s) would fail on a precondition.

A manifest that names its own metadata.namespace is checked there — that is where kubectl apply will put it, and it decides the pod-security level and where Secrets are looked up. -n (or your kubeconfig's namespace) applies only to manifests that omit one.

What it checks

Clusters and standalones

Check Why it matters
spec.storage.className exists The operator refuses to build the StatefulSet and records StorageClassNotFound
That class allows volume expansion Without it spec.storage.size is effectively immutable — discovered during the resize a full disk made urgent
When className is empty, the cluster default is resolved and gets the same expansion check The class is stamped onto the PVC at admission, so it is knowable before anything is scheduled. EKS's in-tree gp2 default has shipped with allowVolumeExpansion unset — an immutable volume size on a cluster where nobody configured storage at all. No default class at all is a problem: every PVC stays Pending. Several classes marked default is a warning naming the one Kubernetes will use (the most recently created)
A Ready node can fit the memory request The top pod-startup failure, invisible to any manifest-level check
spec.image.pullSecrets exist Otherwise every pod sits in ImagePullBackOff

Capacity is compared against a single node's allocatable memory, never the cluster total: the scheduler places a pod on one node, so three nodes with 1Gi each cannot run one 2Gi pod however the total reads.

Backups

Check Why it matters
credentialsSecretRef exists Named but absent means the Job cannot start
It carries every key the Job mounts A missing key gives CreateContainerConfigError, which never mentions backups
Or: the backup ServiceAccount has a cloud-identity annotation With no Secret and no IRSA / Workload Identity binding, the Job runs with no credentials at all
A PVC-backed backup names its claim (spec.storage.pvc.name) and that claim exists, or the operator will create it A nameless PVC is refused by the operator outright (artifacts would land in an EmptyDir and be discarded). When spec.storage.pvc.size is set the operator provisions an absent claim itself, so that is reported as waiting (…) and does not fail the run. With no size nothing is ever created: a missing claim is a failure, because the Job would wait on a volume that does not exist. If the claim cannot be read at all (RBAC, an unhealthy API server), that is a ⚠ saying the check could not be made, not a pass and not a failure. No StorageClass check is made for backup claims

Cross-cluster replicas (backup mode)

Check Why it matters
The downstream cluster or standalone has AWS_REGION (or AWS_DEFAULT_REGION) in its spec.env, in a Secret or ConfigMap of its spec.extraEnvFrom (with the source's prefix applied), or in the environment the operator has already rendered onto its StatefulSet — checked for s3:// URIs only. A source it cannot read (you may not be allowed to read Secrets) gives a warning, not a problem: the operator does not block on it either The seed and every pull run on the Neo4j server, through the AWS SDK's default credential chain — so the object-store settings must be in the server's environment, not on the replica CR. Without it the replica fails inside the SDK with "Unable to load region from any of the providers", which mentions neither replicas nor buckets. A downstream bound to a cloud role through spec.podServiceAccountAnnotations gets a warning instead, because its credentials are injected by the platform and cannot be read here
source.credentialsSecretRef names a Secret that exists The operator projects that Secret onto the downstream servers before it seeds (one rolling restart), so a name with nothing behind it leaves the replica waiting indefinitely
Network mode Says so and checks nothing — reading over the wire needs no bucket credentials

Placement (zones)

Check Why it matters
Enough zones exist for the placement asked for topologySpread defaults to whenUnsatisfiable: DoNotSchedule on topology.kubernetes.io/zone, and antiAffinity.type: required (or enforceDistribution) becomes a required rule on the same key. Three servers across two zones means one stays Pending forever, and nothing in the manifest hints at it
Nodes carry the zone label at all A hard constraint on a topology key no node has can never be satisfied, however many nodes you add — the single-node and bare-metal case

Soft constraints are left alone: ScheduleAnyway and preferred anti-affinity degrade rather than block, which is what they are for.

Pod Security Admission

spec.securityContext replaces the operator's hardened default rather than merging with it. Setting one field to fix a permissions problem silently drops runAsNonRoot, the RuntimeDefault seccomp profile and capabilities.drop: [ALL] along with it.

Check Why it matters
An override in a namespace enforcing restricted or baseline still satisfies it Fatal in the worst-shaped way: the StatefulSet is created and admission rejects its pods, so the CR looks applied, no pod exists, and the reason is only on the StatefulSet's events. Every missing field is named
An override anywhere else A warning — the hardening is still lost, but that is a choice rather than a failure

The operator's own default already satisfies restricted, so a CR with no override is silent.

Cross-cluster replication (the proxy load balancer)

Enabling spec.crossClusterReplication creates a LoadBalancer Service in front of every server's transaction-shipping port. loadBalancerInternal (default true) asks for a private one, and the operator requests it by emitting the union of the AWS, Azure and GKE internal-LB annotations — it cannot know which cloud it is on, and a provider ignores keys it does not recognise.

That works on the three clouds the union covers. On any other, the request is accepted, reported, and silently does nothing.

Check Why it matters
The detected cloud reads one of the annotations the operator emits On a provider that reads none of them, loadBalancerInternal: true has no effect and the load balancer is public, in front of the transaction-shipping port. Set your provider's own annotation under spec.crossClusterReplication.annotations — it is applied after the operator's set and wins on shared keys
The address the Service was actually assigned The decisive one. A public IP on a Service that asked to be internal is proof rather than prediction, so it is reported whatever the annotation table said
loadBalancerInternal: false Said out loud once. It is a deliberate setting, not an error — but the port is then reachable from the internet, and should be restricted with a source-range annotation or a NetworkPolicy, and require TLS

The cloud is detected from a node's spec.providerID, which the kubelet sets — more reliable than node labels, which anyone can edit. On Kind this reports a development limitation (no load-balancer controller, so the Service stays Pending) rather than an exposure risk.

An AWS load balancer is handed out as a hostname, not an IP. Resolving it would be reachability, so preflight reports the hostname as unchecked and gives you the dig command rather than implying it looked.

This replaces the ritual the troubleshooting guide documents — kubectl run backup-auth-check --image=amazon/aws-cli …, in three vendor variants — which you only reach for after a backup has already failed.

What it does not check

Shape, not reachability. It reads Kubernetes objects: a StorageClass, a Secret's key names, a ServiceAccount's annotations, node allocatable capacity. It never contacts S3, GCS or Azure, never talks to a registry, and never runs a probe pod.

So a bucket that exists but denies access still fails at run time, and credentials that are well-formed but wrong still fail at run time. The output says so on every run rather than letting a clean result imply more than it means.

That boundary is deliberate: it keeps the command free of new images, new RBAC and anything that mutates your cluster.

A kind with no cluster-side preconditions is reported as skipped, not passed — a silent pass would imply a check that was never made.

Exit codes

Code Meaning
0 every check passed (warnings alone do not fail)
1 a check failed
2 usage error, or the cluster could not be reached

See also

  • validate — the manifest itself, against the operator's own validators
  • diagnose — the same class of failure, after it has happened
  • Storage Expansion — why allowVolumeExpansion matters before you need it