Skip to content

draincheck

CI Release GitHub Release GitHub License OpenSSF Scorecard Signed with cosign Read-only

Read-only preflight analyzer for multi-node kubectl drain. kubectl drain tells you what is refused on one node, now. draincheck looks at the whole plan, several nodes at once, and tells you what would block it or put something at risk before you cordon anything.

Example

Output from a real cluster, with names anonymized. The text report is compact: findings are grouped by cause (one per PodDisruptionBudget, workload, DaemonSet or node, not one per pod), each remediation is printed once and a list is cut after 5 entries. --verbose prints every finding in full. The JSON report always carries every finding and every pod.

$ draincheck --nodes worker-01,worker-02
draincheck - preflight maintenance analysis
Timestamp: 2026-10-20T10:36:32Z

=== FINDINGS SUMMARY ===
  BLOCKED: 0
  RISK:    37
  UNKNOWN: 1

=== RISK (37) ===

[MISSING_RESOURCE_REQUESTS] 28 findings
  Deployment/argocd/argocd-applicationset-controller: Missing resource requests in 1 of 1 pod of Deployment argocd/argocd-applicationset-controller on the target nodes: argocd-applicationset-controller (cpu, memory). The scheduler treats a missing request as needing nothing, so rescheduled pods may not fit where they land.
  Deployment/argocd/argocd-dex-server: Missing resource requests in 1 of 1 pod of Deployment argocd/argocd-dex-server on the target nodes: dex (cpu, memory). The scheduler treats a missing request as needing nothing, so rescheduled pods may not fit where they land.
  Deployment/argocd/argocd-notifications-controller: Missing resource requests in 1 of 1 pod of Deployment argocd/argocd-notifications-controller on the target nodes: argocd-notifications-controller (cpu, memory). The scheduler treats a missing request as needing nothing, so rescheduled pods may not fit where they land.
  Deployment/argocd/argocd-redis: Missing resource requests in 1 of 1 pod of Deployment argocd/argocd-redis on the target nodes: redis (cpu, memory). The scheduler treats a missing request as needing nothing, so rescheduled pods may not fit where they land.
  Deployment/argocd/argocd-repo-server: Missing resource requests in 1 of 1 pod of Deployment argocd/argocd-repo-server on the target nodes: argocd-repo-server (cpu, memory). The scheduler treats a missing request as needing nothing, so rescheduled pods may not fit where they land.
  ... and 23 more
  Remediation: Set CPU and memory requests on every container (or a pod-level request) so rescheduling and capacity checks can account for the workload.

[POD_OWNERSHIP] 9 findings
  Node/worker-01: Node worker-01 runs 1 static pod (kube-system/kube-proxy-worker-01). Static pods cannot be evicted or rescheduled and stop when the node goes down.
  Node/worker-02: Node worker-02 runs 1 static pod (kube-system/kube-proxy-worker-02). Static pods cannot be evicted or rescheduled and stop when the node goes down.
  DaemonSet/kube-system/cilium: DaemonSet kube-system/cilium has 2 pods on 2 target nodes. kubectl drain refuses DaemonSet pods without --ignore-daemonsets and never evicts them.
  DaemonSet/kube-system/rke2-traefik: DaemonSet kube-system/rke2-traefik has 2 pods on 2 target nodes. kubectl drain refuses DaemonSet pods without --ignore-daemonsets and never evicts them.
  DaemonSet/longhorn-system/engine-image-ei-a4d05f02: DaemonSet longhorn-system/engine-image-ei-a4d05f02 has 2 pods on 2 target nodes. kubectl drain refuses DaemonSet pods without --ignore-daemonsets and never evicts them.
  ... and 4 more
  Remediation (Node/worker-01, Node/worker-02): Plan for the static pods to be unavailable while the node is down; kubectl drain skips them.
  Remediation (DaemonSet/kube-system/cilium, DaemonSet/kube-system/rke2-traefik, DaemonSet/longhorn-system/engine-image-ei-a4d05f02, DaemonSet/longhorn-system/engine-image-ei-db6c2b6f, DaemonSet/longhorn-system/longhorn-csi-plugin and 2 more): Drain with --ignore-daemonsets; DaemonSet pods stay until the node itself goes down.

=== UNKNOWN (1) ===

[PDB_DISRUPTION_BLOCKED] 1 finding
  Namespace/longhorn-system: Operator longhorn.io (InstanceManager) manages 2 PodDisruptionBudgets in namespace longhorn-system with 0 allowed disruptions, covering 2 pods on 2 target nodes. Whether the drain stalls depends on operator state draincheck cannot see, and the operator may remove such budgets when a node is cordoned.
  Remediation: Budgets managed by operator longhorn.io: check its drain documentation and the health of what it protects before draining. Do not relax minAvailable on them; the operator may recreate the budget. Longhorn can block a drain while the node holds the last healthy replica of a volume: check volume health and Longhorn's node drain policy setting before draining.

Some lists are collapsed. Use --verbose to print every finding.

This report describes the observed cluster state at 2026-10-20T10:36:32Z and does not guarantee execution success. Cluster state may drift immediately after analysis.
$ echo $?
0

The exit code is 0 here because no finding reaches the default --fail-on blocked. With --fail-on risk this run would exit 1. See docs/USAGE.md for every flag, rule and exit code.

What it is

  • A read-only Go CLI built on client-go. It lists and reads cluster state; it never cordons, evicts or modifies anything.
  • Findings are BLOCKED, RISK or UNKNOWN. There is deliberately no SAFE.
  • Not a controller and not a scheduler simulator.

Install

Linux, macOS and Windows binaries (amd64 and arm64) are on the releases page. Releases are signed with cosign (keyless) and ship SLSA provenance. Install and verification steps: docs/RELEASE.md.

Usage

draincheck --nodes worker-02,worker-04 [--kubeconfig PATH] [--context NAME] [--output text|json] [--verbose] [--fail-on blocked|risk|any] [--debug]
Flag Description Default
--nodes Nodes to drain, comma-separated, or '*' for all (required) none
--kubeconfig Path to kubeconfig standard loading rules
--context Kubeconfig context current context
--output Output format: text or json text
--verbose Text output only: print every finding in full off
--fail-on Lowest finding level that exits 1: blocked, risk or any blocked
--debug Write one line to stderr for each action; the report on stdout does not change off

Tip

Something looks wrong, or you want to report a problem? Run again with --debug. It writes a line to stderr for each action draincheck takes: the settings it uses, every API call with its object count and time, how the target nodes were resolved, each rule with its number of findings, and the exit code. The report on stdout stays the same, and the lines never contain tokens or kubeconfig contents. See Debug output.

Tip

You usually do not need --kubeconfig or --context. draincheck loads your kubeconfig with the same rules as kubectl: the KUBECONFIG environment variable if set, otherwise ~/.kube/config, using the current context. If kubectl get nodes works for the user running draincheck, draincheck --nodes worker-02 works too. It does not call the kubectl binary; it reads the same kubeconfig and talks to the API server directly, so kubectl does not have to be installed.

FAQ

Why not kubectl drain --dry-run?

Use both. kubectl drain --dry-run=server asks the API server whether each eviction would be accepted now. It takes several node names in one run, but checks them one after another, each against the cluster as it is now. At that it is exact, and draincheck does not try to beat it: a budget that allows 0 disruptions shows up in both. draincheck is a different layer. kubectl answers "what happens if I drain this node now"; draincheck answers "what happens if I do this plan across several nodes, and what should I look at first".

What a server dry run cannot show, and draincheck reports:

  • Several nodes against one budget. A dry-run eviction does not use up the budget, so every node passes, even when a real drain of the same nodes cannot finish (checked with kubectl v1.36.1: three nodes, one pod each, a budget allowing 1; the dry run exits 0, the real drain evicts the first pod, retries the others against the budget until --timeout runs out for each node, and exits 1 with those nodes left cordoned). PDB_BATCH_EXCEEDS_BUDGET adds up the pods of all target nodes behind one PodDisruptionBudget and reports a batch the budget allows only in steps.
  • Operators that react to a cordon. A dry run does not really cordon, so it cannot show what an operator does when the node is cordoned. draincheck marks such budgets UNKNOWN instead of guessing. Checking what the operator does is planned.
  • What the drain leaves behind. Static pods and DaemonSet pods, which kubectl never evicts and which stay until the node goes down.
  • What may not fit when rescheduled. Evicted pods whose containers have no resource requests, because the scheduler treats a missing request as needing nothing.
  • All nodes in one run, with exit codes for CI, and no retry loop.

draincheck reads a snapshot, so it cannot be more exact than the API server on a single eviction, and it does not predict whether evicted pods will fit elsewhere yet (planned).

Build

go test ./...
go build -o draincheck ./cmd/draincheck

Requires Go 1.26+.

Docs

Specification · Rules and noise budget · CI · Release and verification

About this project

Claude

Important

AI-assisted, not vibe-coded. These days AI is part of how code gets written, and it is here too. A human stays in the loop for all of it: I read every change, run it, and commit it signed. The AI proposes, I decide.

Contributions the author cannot explain will be closed. Use any tool you like, but you are accountable for every line you submit: understand it, run it, and be able to defend it in review. Vibe-coding is not welcome here.

Contributing

See CONTRIBUTING.md.

License

Apache-2.0

Releases

Packages

Used by

Contributors

Languages