feat(docker-nuke): add script scaffold, arg parsing, and test harness

This commit is contained in:
David Ibia
2026-07-19 20:32:12 +01:00
parent 25d06b4ce4
commit 36caeafd03
3 changed files with 384 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env bash
# docker-nuke — capture diagnostics from a wedged Docker Desktop, then
# force-kill and restart it.
#
# Written for bash 3.2 compatibility (macOS /bin/bash).
set -euo pipefail
# --- configuration ----------------------------------------------------------
#
# The SC2034 directives below are per-line on purpose, not file-level: each
# constant is consumed by a later task, and its directive is deleted at that
# point. A file-level disable would silence SC2034 for the whole file forever,
# hiding a genuinely dead variable added later. By the final task none of these
# directives should remain.
# shellcheck disable=SC2034
HEALTH_TIMEOUT=6 # seconds to wait on `docker info` before calling it wedged
# shellcheck disable=SC2034
GRACEFUL_WAIT=8 # seconds to let Docker quit politely
# shellcheck disable=SC2034
TERM_WAIT=5 # seconds to wait after SIGTERM before SIGKILL
# shellcheck disable=SC2034
RESTART_TIMEOUT=90 # seconds to wait for the daemon to come back
# shellcheck disable=SC2034
POLL_INTERVAL=2 # seconds between daemon health polls
# shellcheck disable=SC2034
SAMPLE_DURATION=2 # seconds of stack sampling per process
# shellcheck disable=SC2034
SAMPLE_TIMEOUT=10 # hard cap on a single `sample` invocation
# shellcheck disable=SC2034
DUMP_RETENTION=10 # how many dump directories to keep
# shellcheck disable=SC2034
LOG_TAIL_LINES=2000 # lines of Docker log to copy (keeps dumps small)
# shellcheck disable=SC2034
DOCKER_APP="/Applications/Docker.app"
# shellcheck disable=SC2034
DOCKER_LOG_DIR="$HOME/Library/Containers/com.docker.docker/Data/log"
# shellcheck disable=SC2034
DOCKER_RAW="$HOME/Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw"
# shellcheck disable=SC2034
CRASH_DIR="$HOME/Library/Logs/DiagnosticReports"
# Overridable so tests never touch real state.
# shellcheck disable=SC2034
DUMP_ROOT="${DOCKER_NUKE_DUMP_ROOT:-$HOME/.local/state/docker-nuke}"
OPT_FORCE=0
OPT_NO_RESTART=0
OPT_DRY_RUN=0
OPT_DEEP=0
# --- helpers ----------------------------------------------------------------
bin_exists() { command -v "$1" >/dev/null 2>&1; }
log() { printf '%s\n' "$*"; }
warn() { printf '%s\n' "$*" >&2; }
usage() {
cat <<'EOF'
docker-nuke: force-restart a wedged Docker Desktop, capturing diagnostics first
Usage:
docker-nuke [options]
Options:
-f, --force Skip the confirmation shown when Docker looks healthy
-n, --no-restart Kill Docker but do not relaunch it
--dry-run Report what would be killed and dumped; change nothing
--deep Also stop root launchd helpers (com.docker.vmnetd); needs sudo
-h, --help Show this help
Diagnostics are written to ~/.local/state/docker-nuke/<timestamp>/ before
anything is killed. The most recent 10 dumps are kept.
Exit codes:
0 Docker killed and confirmed responsive again
1 Daemon did not come back within the restart timeout
2 Usage error, or aborted at the healthy-daemon prompt
EOF
}
parse_args() {
OPT_FORCE=0
OPT_NO_RESTART=0
OPT_DRY_RUN=0
OPT_DEEP=0
while [ "$#" -gt 0 ]; do
# shellcheck disable=SC2034 # OPT_* are read by main once it is wired up
case "$1" in
-f|--force) OPT_FORCE=1 ;;
-n|--no-restart) OPT_NO_RESTART=1 ;;
--dry-run) OPT_DRY_RUN=1 ;;
--deep) OPT_DEEP=1 ;;
-h|--help) usage; return 3 ;;
*)
warn "docker-nuke: unknown option '$1'"
usage >&2
return 2
;;
esac
shift
done
return 0
}
# --- entrypoint -------------------------------------------------------------
main() {
local rc=0
parse_args "$@" || rc=$?
if [ "$rc" -eq 3 ]; then return 0; fi
if [ "$rc" -ne 0 ]; then return "$rc"; fi
log "docker-nuke: not yet implemented"
return 0
}
# Only run when executed, not when sourced by the test harness.
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
main "$@"
fi
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# Test harness for docker-nuke. No external dependencies.
# Run: ./test-docker-nuke.sh
set -uo pipefail # deliberately not -e: a failing assertion must not abort the run
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TESTS_RUN=0
TESTS_FAILED=0
TMPROOT=""
setup_tmp() {
TMPROOT="$(mktemp -d)"
export DOCKER_NUKE_DUMP_ROOT="$TMPROOT/dumps"
}
teardown_tmp() {
if [ -n "$TMPROOT" ] && [ -d "$TMPROOT" ]; then
rm -rf "$TMPROOT"
fi
TMPROOT=""
}
pass() {
TESTS_RUN=$((TESTS_RUN + 1))
printf ' ok %s\n' "$1"
}
fail() {
TESTS_RUN=$((TESTS_RUN + 1))
TESTS_FAILED=$((TESTS_FAILED + 1))
printf ' FAIL %s\n %s\n' "$1" "$2"
}
assert_eq() {
local desc="$1" expected="$2" actual="$3"
if [ "$expected" = "$actual" ]; then
pass "$desc"
else
fail "$desc" "expected [$expected] got [$actual]"
fi
}
assert_contains() {
local desc="$1" haystack="$2" needle="$3"
case "$haystack" in
*"$needle"*) pass "$desc" ;;
*) fail "$desc" "expected to contain [$needle] got [$haystack]" ;;
esac
}
assert_not_contains() {
local desc="$1" haystack="$2" needle="$3"
case "$haystack" in
*"$needle"*) fail "$desc" "expected NOT to contain [$needle] got [$haystack]" ;;
*) pass "$desc" ;;
esac
}
# Source the script under test. Its trailing BASH_SOURCE/$0 guard means main
# runs only when the script is executed directly, so sourcing it here defines
# the functions without triggering a real nuke.
setup_tmp
# shellcheck source=/dev/null
source "$SCRIPT_DIR/docker-nuke"
set +e # the sourced script sets -e; undo it for the harness
printf '\n=== argument parsing ===\n'
test_parse_args() {
parse_args --force
assert_eq "--force sets OPT_FORCE" "1" "$OPT_FORCE"
parse_args -n
assert_eq "-n sets OPT_NO_RESTART" "1" "$OPT_NO_RESTART"
parse_args --dry-run
assert_eq "--dry-run sets OPT_DRY_RUN" "1" "$OPT_DRY_RUN"
parse_args --deep
assert_eq "--deep sets OPT_DEEP" "1" "$OPT_DEEP"
parse_args
assert_eq "no flags leaves OPT_FORCE off" "0" "$OPT_FORCE"
parse_args --bogus >/dev/null 2>&1
assert_eq "unknown flag returns 2" "2" "$?"
local help_text
help_text="$(usage)"
assert_contains "usage mentions --dry-run" "$help_text" "--dry-run"
assert_contains "usage mentions --deep" "$help_text" "--deep"
}
test_parse_args
printf '\n=== summary ===\n'
teardown_tmp
printf 'ran %d, failed %d\n' "$TESTS_RUN" "$TESTS_FAILED"
[ "$TESTS_FAILED" -eq 0 ]