This file is for AI agents (and scripts) that need to set up or drive kexplain for a user. Everything here is non-interactive and machine-checkable.
kexplain explains Karpenter provisioning decisions: which pods triggered a
node, which constraints narrowed the choice, what EC2 CreateFleet picked and
why, and what disruption removed it. Single-file Python 3.8+, stdlib only,
no dependencies to install.
# 1. install (any dir on PATH)
pipx install git+https://github.com/inceptionstack/karpenter-explain
# or: pip install --user git+https://github.com/inceptionstack/karpenter-explain
# or copy the single file: cp kexplain.py ~/.local/bin/kexplain && chmod +x ~/.local/bin/kexplain
# 2. point kubectl at the user's cluster
aws eks update-kubeconfig --name <cluster> --region <region>
# 3. verify everything, machine-readable
kexplain doctor --jsondoctor --json prints {"ok": true|false, "checks": [...]} where each check
has check, ok, detail, and (on failure) fix with the exact remediation.
Exit code 0 means ready, 3 means something required is broken. The checks
aws ec2 access and debug logging are optional: ok stays true without
them, but prices/funnel/plan/why-not need the former and candidate lists need
the latter.
If doctor says karpenter runs in a nonstandard namespace, set
KARPENTER_NAMESPACE=<ns>. If the store path is not writable, set
KEXPLAIN_STORE=<dir>.
Every regular command runs a fast preflight first and falls through to
doctor output automatically when the basics are broken, so you will never
get a raw stack trace for a missing kubeconfig.
Do NOT use kexplain wizard (it requires a TTY and will exit immediately for
you). Use the direct commands:
| Task | Command |
|---|---|
| health check | kexplain doctor --json |
| harvest state (cron this for history retention) | kexplain sync |
| list nodes incl. deleted ones | kexplain nodes --json |
| decision timeline | kexplain history --json --since 24 |
| full trace for a node | kexplain --no-color explain <target> --json |
| why type X was rejected | kexplain --no-color explain <target> --why-not <type> |
| simulate a deployment | kexplain --no-color plan -f <file> |
<target> accepts a node name, nodeclaim name, EC2 instance id, or any
unique prefix.
doctor, nodes, history, and explain all support --json; prefer it
when parsing. explain --json returns the full story object (timestamps,
trigger pods with scheduling reasons, candidates, disruption). plan is text
only. For human-facing text, pass --no-color.
Exit codes: 0 success, 1 error (bad target, unparseable file), 3 doctor found required checks failing.
- Reads: kubectl (logs, pods, nodes, nodeclaims, nodepools, events), and two
read-only AWS calls (
ec2:DescribeInstanceTypes,ec2:DescribeSpotPriceHistory). It never mutates cluster or AWS state. - Writes: only
~/.kexplain/<cluster>/(orKEXPLAIN_STORE). This store contains node names, private IPs, and instance ids. Do not commit it or send it anywhere. - Every command auto-harvests before running;
--no-syncskips that when you need speed and the store is fresh.
infra/create-cluster.sh creates a real EKS cluster + Karpenter v1.13 in
us-east-1 and costs real money (about $0.35/hr idle). Takes ~20 minutes.
infra/teardown.sh removes everything. Never run either without the user
explicitly asking.
- Version:
__version__in kexplain.py is the single source of truth (pyproject reads it dynamically). The version-bump GitHub Action sets it to 0.1. on every push to main. Do not bump it by hand. - Code lives in the single
kexplain.pyfile, organized by section markers (# --- store,# --- decision model,# --- commands, ...). - Tests:
python3 -m unittest discover tests. They run offline against fixtures captured from a real Karpenter v1.13 cluster; no cluster or AWS needed. Add a fixture-based test when you change log parsing, the funnel, or requirement matching. - Style rules that are hard requirements in this repo: no em-dashes anywhere (docs, comments, output strings), plain direct prose, and commit messages carry no AI attribution of any kind.
- Before pushing:
python3 -m unittest discover testsmust pass andgrep -rnP '\x{2014}' $(git ls-files)(the em-dash gate) must come back empty. CI enforces both. - Local quality gate: enable the pre-commit hook once per clone with
git config core.hooksPath .githooks. It runs the syntax check, the unit tests, and the em-dash gate before each commit, so a bad commit is caught locally instead of in CI. It does not touch the version (the Action owns that).