Skip to content

feat(cli): add kamal doctor deploy readiness diagnostics - #31

Merged
mhenrixon merged 1 commit into
dashfrom
issue-9-doctor-command
Jul 28, 2026
Merged

mhenrixon merged 1 commit into
dashfrom
issue-9-doctor-command

Conversation

@mhenrixon

Copy link
Copy Markdown
Collaborator

Summary

New read-only kamal doctor command (ROADMAP R1, pgbus#212 pattern) that reports OK/WARN/FAIL per check before a deploy and exits 1 on any FAIL:

Check How Where it runs
SSH bare true probe forces a connection per host
Docker docker version per host
Registry real docker login (skipped for local registry) per host
Proxy image docker manifest inspect on the configured proxy image proxy hosts
Proxy version running image tag vs Proxy::Run::MINIMUM_VERSION proxy hosts
Ports ss -ltnH on configured http/https ports; proxy-held = OK proxy hosts
DNS Resolv.getaddresses(domain) ∩ server IPs; mismatch = WARN (CDN-friendly) local
Certificates custom PEM parsed locally; ACME-managed read live via TLS; <14 days = WARN local
  • Kamal::Cli::Main#doctor → Kamal::Cli::Doctor (orchestrator) → Kamal::Cli::Doctor::HostChecks (SSHKit backend injected, App::Boot pattern) + Kamal::Cli::Doctor::EndpointChecks (local DNS/TLS)
  • New command builders: Kamal::Commands::Docker#manifest_available?, Kamal::Commands::Server#listeners_on — argv arrays, no inline shell strings
  • Doctor never crashes on a broken environment: every check rescues at its boundary and reports a FAIL/WARN row instead
  • No new deploy.yml keys; respects --hosts/--roles; no deploy lock (read-only)

Closes #9

Test plan

  • test/cli/doctor_test.rb — 11 cases: all-green, proxy too old / at minimum / not running, port conflict, proxy-held ports, DNS unresolvable / elsewhere, expired + expiring + unreachable TLS certs, local registry skip, unreachable host → SSH FAIL row (no crash)
  • test/commands/docker_test.rb / server_test.rb — new builder assertions
  • bundle exec rubocop --parallel clean; unit suite green apart from pre-existing host-arch/Docker-daemon builder failures (verified identical on pristine main worktree)
  • Manual: kamal doctor against a real target reports pass/fail per check (issue's verify step)

Deviations & judgment calls

Deviations

  • None from the issue scope — all six issue checks implemented, plus a proxy-image fetchability check to cover the "ghcr auth" bullet generically.

Discoveries

  • ROADMAP.md exists only on dash, not on main-rooted branches — read the R1 anchor via git show dash:ROADMAP.md.
  • Branch roots off main (upstream), so MINIMUM_VERSION here is upstream's v0.9.2 and the default proxy repo is basecamp/kamal-proxy. Doctor reads both from config/constants, so the dash merge needs no doctor changes.
  • SSHKit::Backend::Printer (test backend) never sets an exit status, so execute returns false under it — doctor tests stub a catch-all execute → true and override per test.
  • On this machine, pristine main shows 7 pre-existing builder/build failures, not the 2 documented Apple-Silicon ones — the extra 5 are CliBuildTest#test_dev* needing a running Docker daemon plus 2 more arch-dependent asserts. Verified identical on a main worktree; none touch doctor paths.

Judgment calls

  • "ghcr auth" implemented as "proxy image manifest fetchable from the host" using the configured repository — ghcr.io/mhenrixon on dash, Docker Hub upstream. Generic > hardcoded ghcr, keeps the branch upstream-PR-able.
  • Registry check performs a real docker login (mutates ~/.docker/config.json) — exactly what every deploy does, and the only faithful auth check.
  • No deploy lock, no modify block — read-only diagnostics, like kamal details.
  • Checks rescue StandardError at per-host/per-domain boundaries, against the "rescue specific errors" style rule — the doctor contract (pgbus#212) is "never raises on a broken environment"; connection failures span Net::SSH::, Errno::, SocketError, Timeout::Error.
  • Legacy proxy boot_config host-file overrides (deprecated) are ignored when computing the expected proxy image; only proxy.run config + defaults count.
  • Port heuristic: running kamal-proxy is assumed to hold its ports (OK); otherwise any listener on the configured ports is FAIL. No process attribution (ss -p needs root).
  • DNS mismatch is WARN, not FAIL (CDN/LB fronting is legitimate); unresolvable is FAIL. Cert expiry warns under 14 days.
  • Test assertions avoid the proxy image org entirely (kamal-proxy:#{MINIMUM_VERSION}) so this new test file merges into dash without the org-fixup conflicts the playbook requires for older proxy tests.

## Summary
New read-only `kamal doctor` command that reports OK/WARN/FAIL per check
before a deploy: SSH reachability, Docker running, registry login, proxy
image manifest fetchable, running proxy version vs MINIMUM_VERSION, ports
80/443 (or configured) free or proxy-held, DNS resolution of proxy host
domains against server IPs, and TLS certificate expiry (custom PEM parsed
locally, ACME-managed checked live). Exits 1 on any FAIL; never crashes on
a broken environment - failures become failing rows.

Remote checks run per host through Kamal::Cli::Doctor::HostChecks (SSHKit
backend injected, App::Boot pattern); DNS/cert checks run locally in
Kamal::Cli::Doctor::EndpointChecks. Command builders added as
Commands::Docker#manifest_available? and Commands::Server#listeners_on.

## Test Coverage
- all-green run incl. per-check output rows and exit 0
- proxy version too old / running-at-minimum / not-running paths
- port conflict FAIL, proxy-held ports OK
- DNS unresolvable FAIL, DNS-elsewhere WARN (CDN-friendly)
- expired custom cert FAIL, expiring-soon WARN, unreachable TLS WARN
- local registry skips docker login
- unreachable host becomes an SSH FAIL row, not a crash

## Verification
- [x] bundle exec rubocop --parallel passes
- [x] unit tests pass (only pre-existing host-arch/Docker-daemon builder
      failures remain; verified identical on pristine main)

Refs #9
@mhenrixon mhenrixon self-assigned this Jul 28, 2026
@mhenrixon
mhenrixon merged commit 6f46e95 into dash Jul 28, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

R1: kamal doctor diagnostics command

1 participant