Status: Draft / Proposed v0.1 License: 100% free and open source (Apache-2.0). No paid tiers, no commercial upsells, no enterprise editions.
Every Kubernetes operator eventually needs to patch, upgrade or reboot a set of worker nodes (for example worker-02 and worker-04). They run kubectl drain on each, and halfway through the operation stalls or causes unexpected downtime.
kubectl drain is an executing command, not a planning tool. It evicts pods one by one and discovers blockers only when it hits them:
- A PodDisruptionBudget (PDB) is misconfigured or allows 0 disruptions, which halts the drain.
- A legacy deployment has no resource requests, so rescheduling is rejected or unpredictable.
- Hard pod anti-affinity rules leave evicted pods with nowhere to land.
- Several nodes are drained at once, starving the remaining nodes of CPU and memory.
There is no standard, read-only preflight tool that evaluates a multi-node maintenance plan across the whole cluster before a single node is tainted, cordoned or drained. Operators write brittle bash scripts, or find out at 2:00 AM in production.
- A read-only CLI tool written in Go that inspects cluster state through the standard Kubernetes client libraries (
client-go). - A preflight risk and constraint analyzer: it reads cluster state, analyzes a target list of nodes, and outputs a structured report of known blockers, configuration risks and blind spots.
- Designed for CI/CD and runbooks: clean JSON plus a human-readable summary, so it can run in a pull request or a pre-maintenance checklist.
- An active controller. It never cordons nodes, evicts pods, modifies PDBs or mutates cluster state. It is strictly non-destructive.
- A full scheduler simulator. Replicating
kube-schedulerlogic creates false confidence. draincheck validates hard static constraints and labels complex or unknown cases asUNKNOWN. - A commercial product. It is a community utility built by operators, for operators.
draincheck deliberately has no SAFE status. In a distributed system you cannot guarantee success in advance. Every finding is in exactly one of three states:
| State | Definition | Example |
|---|---|---|
BLOCKED |
A known constraint definitively prevents or rejects the planned operation under the observed state. | A PDB requires 3 healthy replicas, only 3 exist, so 0 disruptions are allowed. |
RISK |
A configuration or workload condition could cause disruption, downtime or scheduling friction. | A deployment lacks resource requests, or relies on host-port bindings. |
UNKNOWN |
The tool cannot establish the outcome reliably due to unsupported constraints, custom webhooks or dynamic races. | A custom scheduler plugin or complex cloud storage attachment topology is in use. |
kubectl drain does not give up on a refused eviction: it retries until --timeout, which is infinite by default. The two states are therefore defined by whether waiting can fix the problem:
BLOCKED: the drain will not complete from the observed state without outside action (a change to the cluster, the workload or the budget). Waiting does not fix it.RISK: the drain proceeds, possibly in steps or after waiting, but something is lost, degraded or delayed.UNKNOWN: draincheck cannot establish which of the two applies.
A PodDisruptionBudget at 0 is the reference case for BLOCKED. The disruption controller sets disruptionsAllowed to currentHealthy - desiredHealthy. At 0 the eviction is refused and the pod stays, so no replacement is created and currentHealthy does not rise because of the drain; only something outside the drain can lift it. A budget that allows at least one disruption is different: the first eviction frees a replacement, the budget recovers when that replacement is Ready, and the drain proceeds in steps.
| Situation | State | Why |
|---|---|---|
| A budget selects pods on the target nodes and allows 0 disruptions | BLOCKED |
The eviction is refused and retrying does not change the count. |
| A budget allows 1 disruption, the batch evicts 3 pods of it | RISK (PDB_BATCH_EXCEEDS_BUDGET) |
The drain waits for each replacement to be Ready; it proceeds. |
| Every node a workload may run on is drained | RISK (planned) |
The drain itself completes; the evicted pods stay Pending and the workload is down. |
| The numbers needed to tell (a stale budget status, an operator-managed budget) are not reliable | UNKNOWN |
Nothing can be said about the outcome. |
The report is a snapshot, as the disclaimer below says. A state that clears by itself, such as a rollout finishing or a pod becoming Ready, is not seen.
Every report carries this mandatory disclaimer:
This report describes the observed cluster state at timestamp T and does not guarantee execution success. Cluster state may drift immediately after analysis.
v0.1 focuses on five checks:
- PDB constraints: disruption budgets that may prevent or delay eviction; correct handling of overlapping selectors (a pod matching multiple PDBs) and unhealthy pod states.
- Pod ownership: DaemonSet-managed pods, static/mirror pods and unmanaged naked pods, which have special drain or eviction behavior.
- Placement constraints: hard node selectors, required node affinity, taints/tolerations and required pod anti-affinity.
- Capacity uncertainty: whether eligible destination nodes have enough allocatable CPU and memory for affected pods, explicitly disclaiming full scheduler priority ranking.
- Multi-node grouping: the proposed set of target nodes assessed together, catching shared disruption budget conflicts across the whole maintenance batch.
draincheck/
├── cmd/draincheck/ # CLI entrypoint (Cobra flags and execution)
├── internal/
│ ├── inventory/ # Read-only ingestion of nodes, pods, PDBs and storage
│ ├── analyzer/ # Pure, stateless rule evaluation logic
│ ├── findings/ # Strongly typed finding definitions (BLOCKED, RISK, UNKNOWN)
│ └── report/ # Human-readable console output and JSON formatter
├── testdata/ # Mock Kubernetes objects for unit tests
└── tests/ # Integration tests (fake client, envtest, kind)
- Typed evidence. Rules return concrete, versioned Go structs, not
map[string]interface{}, so the JSON schema stays stable for CI pipelines. - Immutable snapshots. The inventory collector takes a read-only snapshot of cluster state; rules evaluate that snapshot deterministically.
- Sorted findings. All findings are sorted consistently by namespace, resource kind and name before printing or serialization.
- Accountable actions. Every action draincheck takes is accounted for by a
DEBUGline when--debugis set: each cluster read, each rule run and each decision that shapes the result. The lines go to stderr through an injected*slog.Logger, never to stdout, and never carry tokens or kubeconfig contents. See USAGE.md. - Exit codes are the CI/CD contract.
0: the analysis completed and no finding reaches--fail-on.1: the analysis completed and at least one finding reaches--fail-on.2: draincheck could not complete (bad flags, unreadable kubeconfig, unreachable cluster, unknown node). The report is written before the code is decided.--debugnever changes the code. One limit applies: when stdout or stderr is a pipe whose reader has already exited, Go ends the process withSIGPIPEand the shell reports141in place of0,1or2. This applies to stdout with or without--debug, and to stderr only with--debug, because without it stderr is written only on failure. A gate must not pipe either stream into a command that quits early, and should treat141as a failed run. See USAGE.md.
$ draincheck --nodes "worker-02,worker-04"
[draincheck v0.1.0] - Preflight Maintenance Analysis
Cluster: production-lon-01 | Timestamp: 2026-10-10T21:00:00Z
Target Nodes: [worker-02, worker-04]
=== FINDINGS SUMMARY ===
BLOCKED: 1
RISK: 2
UNKNOWN: 1
...