Last active
September 2, 2026 16:14
-
-
Save wdhowe/726bc72ce086ffeb041f08b4d5964eb6 to your computer and use it in GitHub Desktop.
bashrc/profile
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ##-- Aliases --## | |
| alias ll='ls -l' | |
| alias k='kubectl' | |
| alias ke='kubectl exec -it' | |
| alias kc='kubectx' | |
| alias kn='kubens' | |
| ##-- Completions and Prompt --## | |
| # bash-completion@2 (installed via brew install bash-completion@2) | |
| [[ -r "/opt/homebrew/etc/profile.d/bash_completion.sh" ]] && . "/opt/homebrew/etc/profile.d/bash_completion.sh" | |
| # task completion (go-task / taskfile) | |
| command -v task &>/dev/null && eval "$(task --completion bash)" | |
| # direnv load/unload environments automatically (installed via brew install direnv) | |
| eval "$(direnv hook bash)" | |
| # bash-git-prompt (installed via brew install bash-git-prompt) | |
| if [ -f "/opt/homebrew/opt/bash-git-prompt/share/gitprompt.sh" ]; then | |
| __GIT_PROMPT_DIR="/opt/homebrew/opt/bash-git-prompt/share" | |
| source "/opt/homebrew/opt/bash-git-prompt/share/gitprompt.sh" | |
| fi | |
| ##-- Utility Functions --## | |
| function nth() { | |
| # Print the nth line of stdin. | |
| # Usage: <command> | nth <line-number> | |
| sed -n "${1}p" | |
| } | |
| ##-- Shared Helpers --## | |
| function _confirm() { | |
| # Ask before a step that could destroy work existing nowhere else. True when | |
| # the answer is yes. Declines when there is no terminal to answer on. | |
| # Usage: _confirm <prompt> | |
| # Env: CONFIRM_ASSUME_YES=1 answer yes without asking | |
| local prompt="$1" | |
| local reply | |
| if [ "${CONFIRM_ASSUME_YES:-}" = 1 ]; then | |
| echo " ${prompt} y (CONFIRM_ASSUME_YES=1)" | |
| return 0 | |
| fi | |
| if [ ! -t 0 ]; then | |
| return 1 | |
| fi | |
| read -r -p " ${prompt} [y/N] " reply | |
| case "$reply" in | |
| [yY]*) return 0 ;; | |
| *) return 1 ;; | |
| esac | |
| } | |
| function _gh() { | |
| # Run gh under its own keyring login. | |
| # Usage: _gh <gh-arguments...> | |
| ( | |
| # .envrc exports a PAT in some repositories, and it shadows that login. | |
| unset GH_TOKEN GITHUB_TOKEN | |
| command gh "$@" | |
| ) | |
| } | |
| function _gh-pr() { | |
| # Print the newest pull request for a head branch as jq output. Empty when | |
| # there is none, or when gh cannot answer. | |
| # Usage: _gh-pr <branch> <state> <json-fields> <jq-expression> | |
| _gh pr list --head "$1" --state "$2" --limit 1 --json "$3" --jq "$4" 2>/dev/null | |
| } | |
| ##-- Git Functions --## | |
| function _git-branch-exists() { | |
| # True when a local branch of this name exists. | |
| # Usage: _git-branch-exists <branch> | |
| git show-ref --verify --quiet "refs/heads/$1" | |
| } | |
| function _git-primary-worktree() { | |
| # Print the absolute path of the repository's primary worktree, or nothing | |
| # outside a repository. | |
| # `git worktree list` always prints the primary first, so a linked worktree | |
| # can find its way home. | |
| git worktree list --porcelain 2>/dev/null | sed -n '1s/^worktree //p' | |
| } | |
| function _git-linked-worktrees() { | |
| # Print every worktree except the primary, in `git worktree list` form. | |
| git worktree list | tail -n +2 | |
| } | |
| function _git-integration-branch() { | |
| # Print the branch that sweeps measure against: main, master, or HEAD when | |
| # neither exists. | |
| local branch | |
| # Named explicitly rather than left as HEAD, so a sweep run from a | |
| # topic-branch worktree does not widen to "merged into whatever is checked out". | |
| for branch in main master; do | |
| if _git-branch-exists "$branch"; then | |
| echo "$branch" | |
| return 0 | |
| fi | |
| done | |
| echo HEAD | |
| } | |
| function _git-require-clean-tree() { | |
| # True when a worktree has no uncommitted changes, staged or unstaged. Warns | |
| # when it does. | |
| # Usage: _git-require-clean-tree [worktree-path] (default the current directory) | |
| local dir="${1:-.}" | |
| if ! git -C "$dir" diff --quiet 2>/dev/null || | |
| ! git -C "$dir" diff --cached --quiet 2>/dev/null; then | |
| echo "Warning: Uncommitted changes in ${dir}. Stash or commit them first." | |
| return 1 | |
| fi | |
| } | |
| function _git-sync-main() { | |
| # Fast-forward main in the primary worktree. Callers that need the path ask | |
| # _git-primary-worktree. | |
| local primary | |
| # main can only be checked out in the primary worktree, so doing this by path | |
| # is what lets a caller run from a linked worktree, where a plain | |
| # `git checkout main` fails with "already used by worktree". | |
| primary=$(_git-primary-worktree) | |
| if [ -z "$primary" ]; then | |
| echo "Error: Not in a git repository" | |
| return 1 | |
| fi | |
| _git-require-clean-tree "$primary" || return 1 | |
| git -C "$primary" checkout main && git -C "$primary" pull | |
| } | |
| ##-- Git Worktree Release --## | |
| function _git-scan-cwds() { | |
| # Snapshot every process's current directory into _GIT_CWD_SCAN, once per | |
| # sweep. Callers starting a sweep unset _GIT_CWD_SCAN first to force a refresh. | |
| [ -n "${_GIT_CWD_SCAN+set}" ] && return 0 | |
| # `lsof -d cwd` reads the process table rather than walking any tree, so one | |
| # pass costs a fraction of a second. One pass per branch would not. | |
| if command -v lsof >/dev/null 2>&1; then | |
| _GIT_CWD_SCAN=$(lsof -d cwd -Fn 2>/dev/null | sed -n 's|^n/|/|p') | |
| else | |
| _GIT_CWD_SCAN="" | |
| fi | |
| } | |
| function _git-worktree-in-use() { | |
| # True when a process sits in the worktree: another shell, an editor, an agent | |
| # session, a dev server. Reads the snapshot _git-scan-cwds took. | |
| # Usage: _git-worktree-in-use <worktree-path> | |
| local worktree="$1" | |
| [ -n "$_GIT_CWD_SCAN" ] || return 1 | |
| printf '%s\n' "$_GIT_CWD_SCAN" | | |
| awk -v w="$worktree" ' | |
| index($0, w) == 1 && | |
| (length($0) == length(w) || substr($0, length(w) + 1, 1) == "/") { found = 1 } | |
| END { exit !found }' | |
| } | |
| function _git-worktree-block-reason() { | |
| # Print why a linked worktree must not be released, or nothing when it may be. | |
| # Changes nothing. | |
| # Usage: _git-worktree-block-reason <worktree-path> | |
| local worktree="$1" | |
| local here | |
| # `pwd -P`, not $PWD: git records the physical path, and on macOS /tmp and | |
| # /var are symlinks, so a logical cwd would never match and the caller would | |
| # delete the directory it is standing in. | |
| here=$(pwd -P 2>/dev/null) | |
| if [ "$worktree" = "$(_git-primary-worktree)" ]; then | |
| echo "checked out in the primary worktree" | |
| return 0 | |
| fi | |
| case "${here}/" in | |
| "${worktree}"/*) | |
| echo "you are standing in worktree ${worktree}" | |
| return 0 | |
| ;; | |
| esac | |
| if [ -n "$(git -C "$worktree" status --porcelain 2>/dev/null)" ]; then | |
| echo "uncommitted work in worktree ${worktree}" | |
| return 0 | |
| fi | |
| if _git-worktree-in-use "$worktree"; then | |
| echo "a running process is working in ${worktree}" | |
| return 0 | |
| fi | |
| } | |
| function _git-worktree-ignored-ok() { | |
| # True when a worktree's ignored files may be destroyed along with it. Lists | |
| # them and asks first when it has any. | |
| # Usage: _git-worktree-ignored-ok <worktree-path> | |
| # Env: CONFIRM_ASSUME_YES=1 answer yes without asking | |
| local worktree="$1" | |
| local count | |
| # `status --porcelain` hides ignored paths and `git worktree remove` deletes | |
| # them without comment, so an untracked .env.dev or a populated .terraform | |
| # would vanish silently. Regenerating node_modules is cheap, a secret is not. | |
| count=$(git -C "$worktree" status --porcelain --ignored 2>/dev/null | grep -c '^!!') | |
| [ "$count" -gt 0 ] || return 0 | |
| echo " Removing ${worktree} would also delete ${count} ignored path(s):" | |
| git -C "$worktree" status --porcelain --ignored 2>/dev/null | | |
| sed -n 's/^!! / /p' | head -5 | |
| if [ "$count" -gt 5 ]; then | |
| echo " ... and $((count - 5)) more" | |
| fi | |
| _confirm "Delete worktree ${worktree} and those files?" | |
| } | |
| function _git-release-worktree() { | |
| # Remove the worktree holding a branch, so the branch itself can be deleted. | |
| # True when the branch had no worktree or its worktree was removed; prints the | |
| # reason and returns false when the worktree must stay. | |
| # Usage: _git-release-worktree <branch> | |
| local branch="$1" | |
| local worktree | |
| local reason | |
| worktree=$(git for-each-ref --format='%(worktreepath)' "refs/heads/${branch}") | |
| [ -n "$worktree" ] || return 0 | |
| _git-scan-cwds | |
| reason=$(_git-worktree-block-reason "$worktree") | |
| if [ -n "$reason" ]; then | |
| echo " Skipped ${branch}: ${reason}" | |
| return 1 | |
| fi | |
| if ! _git-worktree-ignored-ok "$worktree"; then | |
| echo " Skipped ${branch}: kept worktree holding ignored files" | |
| return 1 | |
| fi | |
| if ! git worktree remove "$worktree"; then | |
| echo " Skipped ${branch}: could not remove worktree ${worktree}" | |
| return 1 | |
| fi | |
| echo " Removed worktree ${worktree}" | |
| } | |
| ##-- Git Branch Sweeps --## | |
| function _git-branch-pr-state() { | |
| # Print what GitHub says became of a branch, as "STATE|number". STATE is | |
| # MERGED, CLOSED, OPEN, NONE when no pull request was ever opened, or UNKNOWN | |
| # when gh cannot answer. | |
| # Usage: _git-branch-pr-state <branch> | |
| # Env: GIT_CLEAN_NO_GH=1 skip the lookup and report UNKNOWN | |
| local answer | |
| if [ "${GIT_CLEAN_NO_GH:-}" = 1 ] || ! command -v gh >/dev/null 2>&1; then | |
| echo "UNKNOWN|" | |
| return 0 | |
| fi | |
| # Authoritative where `git cherry` is not: a squash-merge of a multi-commit | |
| # branch rewrites every patch-id, so cherry calls a merged branch unmerged. | |
| answer=$(_gh-pr "$1" all 'state,number' '"\(.[0].state // "NONE")|\(.[0].number // "")"') | |
| echo "${answer:-UNKNOWN|}" | |
| } | |
| function _git-vet-forced-delete() { | |
| # True when a branch whose upstream vanished is safe to force-delete. Asks | |
| # first, and prints the reason and returns false, when it is not. | |
| # Usage: _git-vet-forced-delete <branch> | |
| # Env: CONFIRM_ASSUME_YES=1 answer yes without asking | |
| local branch="$1" | |
| local base | |
| local pr | |
| local state | |
| local number | |
| local unmerged | |
| # A gone upstream normally means "PR merged, remote branch deleted", and the | |
| # branch is then safe to discard even though ancestry cannot vouch for it | |
| # after a squash-merge. Everything here checks that story holds. | |
| pr=$(_git-branch-pr-state "$branch") | |
| state=${pr%%|*} | |
| number=${pr#*|} | |
| case "$state" in | |
| MERGED) return 0 ;; | |
| OPEN) | |
| echo " Skipped ${branch}: PR #${number} is open but its remote head is gone" | |
| return 1 | |
| ;; | |
| esac | |
| # CLOSED, no PR at all, or gh could not answer. Fall back to patch | |
| # containment, which over-reports on squash-merges but never under-reports. | |
| base=$(_git-integration-branch) | |
| unmerged=$(git cherry "$base" "refs/heads/${branch}" | grep -c '^+') | |
| [ "$unmerged" -gt 0 ] || return 0 | |
| echo " ${branch}: upstream gone, ${state} on GitHub, ${unmerged} commit(s) not in ${base}:" | |
| git cherry -v "$base" "refs/heads/${branch}" | sed -n 's/^+ / /p' | head -5 | |
| if ! _confirm "Force-delete ${branch} anyway?"; then | |
| echo " Skipped ${branch}: unmerged commits, upstream gone" | |
| return 1 | |
| fi | |
| } | |
| function _git-remove-branch() { | |
| # Delete one local branch, releasing its worktree first when it has one. | |
| # Usage: _git-remove-branch <branch> [--force] | |
| local branch="$1" | |
| local force="$2" | |
| # Every check runs before anything is destroyed, so a branch is never left | |
| # behind with its worktree already gone. Only the forced path needs vetting: | |
| # without --force the branch is an ancestor of the integration branch, so its | |
| # commits are already there. | |
| if [ "$force" = "--force" ]; then | |
| _git-vet-forced-delete "$branch" || return 0 | |
| _git-release-worktree "$branch" || return 0 | |
| git branch --delete --force "$branch" | |
| else | |
| _git-release-worktree "$branch" || return 0 | |
| git branch --delete "$branch" | |
| fi | |
| } | |
| function _git-branches-with-gone-upstream() { | |
| # Print the local branches whose remote head has been deleted. | |
| # Read from for-each-ref, not `git branch`: the latter marks a branch checked | |
| # out in a worktree with a `+` and swaps the upstream column for the worktree | |
| # path, so column-parsing it yields "+" as the branch name. | |
| git for-each-ref --format='%(refname:short)%09%(upstream:track)' refs/heads | | |
| awk -F'\t' '$2 == "[gone]" { print $1 }' | |
| } | |
| function _git-branches-merged-into() { | |
| # Print the local branches already contained in <base>, main and master aside. | |
| # Usage: _git-branches-merged-into <base> | |
| # Matched with -x so a name like topic/domain-main is not mistaken for main. | |
| git for-each-ref --merged "$1" --format='%(refname:short)' refs/heads | | |
| grep -vx -e main -e master | |
| } | |
| function _git-sweep-branches() { | |
| # Remove every branch in a newline-separated list, or report there were none. | |
| # Usage: _git-sweep-branches <none-message> <branches> [--force] | |
| local none="$1" | |
| local branches="$2" | |
| local force="$3" | |
| local branch | |
| if [ -z "$branches" ]; then | |
| echo " ${none}" | |
| return 0 | |
| fi | |
| while IFS= read -r branch; do | |
| [ -n "$branch" ] || continue | |
| _git-remove-branch "$branch" "$force" | |
| done <<<"$branches" | |
| } | |
| function _git-report-worktrees() { | |
| # Print the linked worktrees that survived a sweep, so a skipped branch is | |
| # easy to go and finish. | |
| local linked | |
| linked=$(_git-linked-worktrees) | |
| if [ -n "$linked" ]; then | |
| echo "Remaining worktrees:" | |
| echo "$linked" | sed 's/^/ /' | |
| fi | |
| } | |
| function git-rm-local-merged-branches() { | |
| # Delete the local branches whose upstream is gone, then those already merged | |
| # into the integration branch, releasing their worktrees first. Anything | |
| # unsafe to touch is skipped with a printed reason. | |
| # Env: CONFIRM_ASSUME_YES=1 answer every prompt yes | |
| # GIT_CLEAN_NO_GH=1 skip the pull-request lookups | |
| local base | |
| # Force one fresh process-cwd snapshot for this sweep. | |
| unset _GIT_CWD_SCAN | |
| base=$(_git-integration-branch) | |
| echo "Removing branches with deleted upstreams..." | |
| _git-sweep-branches "No branches with deleted upstreams" \ | |
| "$(_git-branches-with-gone-upstream)" --force | |
| echo "Removing branches merged into ${base} (exclude main,master)..." | |
| _git-sweep-branches "No merged branches to remove" \ | |
| "$(_git-branches-merged-into "$base")" | |
| } | |
| function git-clean() { | |
| # Fetch with pruning, then sweep the local branches and worktrees the remote | |
| # no longer has. | |
| # Env: CONFIRM_ASSUME_YES=1 answer every prompt yes | |
| # GIT_CLEAN_NO_GH=1 skip the pull-request lookups | |
| echo "Fetching latest changes and pruning deleted branches..." | |
| git fetch --prune | |
| git worktree prune | |
| git-rm-local-merged-branches | |
| _git-report-worktrees | |
| } | |
| ##-- Git Workflow --## | |
| function git-fresh-main() { | |
| # Update main in the primary worktree, then sweep merged branches and their | |
| # worktrees. Runs from a linked worktree too, and leaves you where you were. | |
| # Env: CONFIRM_ASSUME_YES=1 answer every prompt yes | |
| # GIT_CLEAN_NO_GH=1 skip the pull-request lookups | |
| local primary | |
| primary=$(_git-primary-worktree) | |
| echo "Checking out a fresh main branch${primary:+ in ${primary}}..." | |
| _git-sync-main && git-clean | |
| } | |
| function _git-allow-branch-write() { | |
| # True when a destructive branch command may touch this branch. Refuses main | |
| # and master. | |
| # Usage: _git-allow-branch-write <branch> <what-would-happen> | |
| local branch="$1" | |
| local action="$2" | |
| # The mk/rm names read as general-purpose tools, unlike the create-dev and | |
| # delete-dev they replaced, so the shared branch needs an explicit refusal | |
| # rather than relying on nobody typing it. | |
| case "$branch" in | |
| main | master) | |
| echo "Error: Refusing to ${action} ${branch}, the integration branch" | |
| return 1 | |
| ;; | |
| esac | |
| return 0 | |
| } | |
| function git-mk-branch() { | |
| # Create a branch from an up-to-date main and push it upstream. Checks the | |
| # branch out instead when it already exists. | |
| # Usage: git-mk-branch [branch] (default topic/bill-dev) | |
| local branch="${1:-topic/bill-dev}" | |
| local primary | |
| if _git-branch-exists "$branch"; then | |
| echo "Local branch ${branch} already exists, checking it out..." | |
| git checkout "$branch" | |
| return | |
| fi | |
| echo "Creating ${branch} from main..." | |
| _git-sync-main || return 1 | |
| # Created in the primary worktree, where _git-sync-main just left main, so the | |
| # new branch starts from main rather than from a linked worktree's HEAD. | |
| primary=$(_git-primary-worktree) | |
| git -C "$primary" checkout -b "$branch" && | |
| git -C "$primary" push -u origin "$branch" | |
| } | |
| function git-rm-branch() { | |
| # Delete a branch locally and on origin, releasing its worktree first. Refuses | |
| # main and master. | |
| # Usage: git-rm-branch [branch] (default topic/bill-dev) | |
| # Env: CONFIRM_ASSUME_YES=1 answer every prompt yes | |
| local branch="${1:-topic/bill-dev}" | |
| _git-allow-branch-write "$branch" "delete" || return 1 | |
| echo "Deleting ${branch} locally and on origin..." | |
| _git-sync-main || return 1 | |
| if _git-branch-exists "$branch"; then | |
| # Both halves or neither. Deleting only the remote would leave the local | |
| # branch with a gone upstream, which the next git-clean reads as "PR merged, | |
| # safe to force-delete" -- the exact work this just declined to touch. | |
| if ! _git-release-worktree "$branch"; then | |
| echo " Keeping the remote branch too, so ${branch} keeps its upstream" | |
| return 1 | |
| fi | |
| git branch --delete --force "$branch" || return 1 | |
| else | |
| echo " Local branch not found, skipping" | |
| fi | |
| if git ls-remote --exit-code --heads origin "$branch" &>/dev/null; then | |
| git push origin --delete "$branch" | |
| else | |
| echo " Remote branch not found, skipping" | |
| fi | |
| } | |
| function git-undo-last-commit() { | |
| # Undo the last commit, keeping its changes staged. | |
| if ! git rev-parse HEAD~1 &>/dev/null; then | |
| echo "Error: No commit to undo (initial commit or empty repo)" | |
| return 1 | |
| fi | |
| echo "Undoing last commit, keeping changes staged..." | |
| git reset HEAD~1 | |
| } | |
| function gh-pr-create-retry() { | |
| # Open a pull request, retrying so a GitHub write outage does not need | |
| # babysitting. Exits early when a PR for the branch already exists. | |
| # Usage: gh-pr-create-retry <title> <body-file> [branch] [base] | |
| # branch defaults to the current one, base to main | |
| # Env: PR_ATTEMPTS how many tries (default 12) | |
| # PR_INTERVAL seconds between tries (default 180) | |
| local title="$1" | |
| local body_file="$2" | |
| local branch="${3:-$(git rev-parse --abbrev-ref HEAD 2>/dev/null)}" | |
| local base="${4:-main}" | |
| local attempts="${PR_ATTEMPTS:-12}" | |
| local interval="${PR_INTERVAL:-180}" | |
| local ls_status | |
| if [ -z "$title" ] || [ -z "$body_file" ]; then | |
| echo "Usage: gh-pr-create-retry \"Title of the PR\" /path/to/body.md [branch] [base]" | |
| return 1 | |
| fi | |
| if ! command -v gh &>/dev/null; then | |
| echo "Error: gh CLI not found on PATH" | |
| return 1 | |
| fi | |
| if [ ! -r "$body_file" ]; then | |
| echo "Error: Body file not found or unreadable: ${body_file}" | |
| return 1 | |
| fi | |
| if [ -z "$branch" ]; then | |
| echo "Error: Not in a git repository, pass the branch explicitly" | |
| return 1 | |
| fi | |
| if [ "$branch" = "$base" ]; then | |
| echo "Error: Head branch and base are both ${base}" | |
| return 1 | |
| fi | |
| # Exit 2 means the branch is genuinely absent from origin; any other failure is | |
| # treated as "network is unhappy", which is exactly what the retry loop is for. | |
| git ls-remote --exit-code --heads origin "$branch" &>/dev/null | |
| ls_status=$? | |
| if [ "$ls_status" -eq 2 ]; then | |
| echo "Error: Branch ${branch} is not on origin. Push it first: git push -u origin ${branch}" | |
| return 1 | |
| fi | |
| ( | |
| err_file="$(mktemp -t gh-pr-create-err)" | |
| trap 'rm -f "$err_file"' EXIT | |
| for ((i = 1; i <= attempts; i++)); do | |
| existing=$(_gh-pr "$branch" open url '.[0].url // empty') | |
| if [ -n "$existing" ]; then | |
| echo "PR already exists: ${existing}" | |
| exit 0 | |
| fi | |
| echo "Attempt ${i}/${attempts} at $(date -u +%H:%M:%SZ): opening PR (${title})..." | |
| if url=$(_gh pr create --base "$base" --head "$branch" --assignee @me \ | |
| --title "$title" --body-file "$body_file" 2>"$err_file"); then | |
| echo "PR created: ${url}" | |
| exit 0 | |
| fi | |
| echo " Failed: $(tr -d '\n' <"$err_file" | tail -c 200)" | |
| if [ "$i" -lt "$attempts" ]; then | |
| sleep "$interval" | |
| fi | |
| done | |
| echo "Error: Gave up after ${attempts} attempts ${interval}s apart, GitHub writes still failing" | |
| exit 1 | |
| ) | |
| } | |
| ##-- K8s Functions --## | |
| function k8s-cached-images() { | |
| # Print every container image cached on any node, digest references included. | |
| kubectl get nodes -o jsonpath="{.items[*].status.images[*].names}" | | |
| tr -s '[[:space:]],' '\n' | | |
| tr -d '"' | | |
| sort -u | |
| } | |
| function k8s-cached-images-tags() { | |
| # Print the cached images without digest references, which comm cannot line up | |
| # against a tag. | |
| k8s-cached-images | grep -v '@sha256:' | |
| } | |
| function k8s-pod-images() { | |
| # Print every container image a running pod uses, in any namespace. | |
| kubectl get pods --all-namespaces -o jsonpath="{..image}" | tr -s '[[:space:]]' '\n' | sort -u | |
| } | |
| function k8s-unused-images() { | |
| # Print the images cached on nodes that no running pod uses. | |
| comm -23 <(k8s-cached-images-tags) <(k8s-pod-images) | |
| } | |
| function _k8s-node-names() { | |
| # Print the name of every node in the current cluster. | |
| kubectl get nodes -o jsonpath='{.items[*].metadata.name}' | |
| } | |
| function _k8s-prune-node-images() { | |
| # Drop every unused image from one kind node, whichever runtime CLI it ships. | |
| # Usage: _k8s-prune-node-images <node> | |
| local node="$1" | |
| docker exec "$node" crictl rmi --prune 2>/dev/null || | |
| docker exec "$node" ctr -n k8s.io images prune 2>/dev/null || | |
| echo " Could not prune images on ${node} (unsupported runtime or not a kind cluster)" | |
| } | |
| function k8s-clear-cached-images() { | |
| # Delete every cached image no running pod uses, from every node. Asks first. | |
| # Intended for kind clusters, whose nodes are docker containers. | |
| # Env: CONFIRM_ASSUME_YES=1 answer yes without asking | |
| local node | |
| echo "Clearing unused container images from all nodes (for kind clusters)..." | |
| echo "Warning: This will delete ALL images not currently used by pods" | |
| echo "Sample of unused images (tag references only):" | |
| k8s-unused-images | |
| echo "" | |
| echo "Note: The cleanup will remove all unused images, including digest references." | |
| echo "" | |
| if ! _confirm "Continue with deletion?"; then | |
| echo "Cancelled." | |
| return 1 | |
| fi | |
| for node in $(_k8s-node-names); do | |
| echo "Cleaning images on node: ${node}" | |
| _k8s-prune-node-images "$node" | |
| done | |
| echo "Done!" | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment