Last active
January 4, 2026 08:47
-
-
Save pvdb/813b5ba5bb1383c526d9dbcde64c9878 to your computer and use it in GitHub Desktop.
kubectl wrapper
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
| #!/usr/bin/env bash | |
| # | |
| # WHAT | |
| # | |
| # kubektl - interactive kubectl wrapper, with caching and history | |
| # | |
| # Dixit "Claude Snonnet 4" -- This is a well-designed utility that significantly improves the developer experience | |
| # when working with Kubernetes clusters, especially in environments with many contexts, namespaces, and resources! | |
| # | |
| # - uses fzf for interactive selection of context/namespace/pod/container | |
| # - caches all API responses locally to speed up subsequent invocations | |
| # - optionally remembers last selections to pre-fill fzf prompts | |
| # | |
| # INSTALLATION | |
| # | |
| # ln -s ${PWD}/kubektl $(brew --prefix)/bin/ | |
| # sudo ln -s ${PWD}/kubektl /usr/local/bin/ | |
| # | |
| # PREREQUISITES | |
| # | |
| # brew install fzf | |
| # brew install lsd | |
| # brew install lolcat | |
| # brew install moreutils | |
| # | |
| # FLAGS | |
| # | |
| # --refresh : invalidate context/namespace/pod/container cache when selecting | |
| # --history : pre-fill fzf with last selected context/namespace/pod/container | |
| # --clear : show cache contents and prompt to delete cache and history | |
| # | |
| set -e ; # CTRL-C or ESC (empty selection) in fzf will exit the script | |
| # define the `kubektl` cache directory, and ensure it exists | |
| # (tree structure: context -> namespace -> pod -> container) | |
| export cache_home=${XDG_CACHE_HOME:-$HOME/.cache}/kubektl ; | |
| mkdir -p "${cache_home}" ; | |
| # define the history file, and ensure it's valid JSON | |
| # (nested JSON: context -> namespace -> pod -> container) | |
| export history_file="${cache_home}/history.json" ; | |
| [[ -s "${history_file}" ]] || echo '{}' > "${history_file}" ; | |
| refresh_cache=false ; # by default do not refresh the cache | |
| use_history=false ; # by default do not use history file | |
| while [[ "$1" == --* ]] ; do | |
| case "$1" in | |
| --refresh) refresh_cache=true ; shift ;; | |
| --history) use_history=true ; shift ;; | |
| --clear) | |
| echo "Cache directory: ${cache_home}" ; | |
| lsd --tree "${cache_home}" ; | |
| echo "" ; | |
| read -p "Clear cache and history? [y/N] " -n 1 -r ; | |
| echo "" ; | |
| if [[ $REPLY =~ ^[Yy]$ ]] ; then | |
| rm -rf "${cache_home:?}"/* ; | |
| echo "Cache cleared." ; | |
| else | |
| echo "Aborted." ; | |
| fi | |
| exit 0 ; | |
| ;; | |
| *) break ;; | |
| esac | |
| done | |
| # read last selected context | |
| read_last_context() { | |
| [[ "${use_history}" == "true" ]] || return ; | |
| jq -r '.last_context // empty' "${history_file}" ; | |
| } | |
| # read last selected namespace for a context | |
| read_last_namespace() { | |
| [[ "${use_history}" == "true" ]] || return ; | |
| local ctx="$1" ; | |
| jq -r --arg ctx "$ctx" '.contexts[$ctx].last_namespace // empty' "${history_file}" ; | |
| } | |
| # read last selected pod for a context/namespace | |
| read_last_pod() { | |
| [[ "${use_history}" == "true" ]] || return ; | |
| local ctx="$1" ; | |
| local ns="$2" ; | |
| jq -r --arg ctx "$ctx" --arg ns "$ns" '.contexts[$ctx].namespaces[$ns].last_pod // empty' "${history_file}" ; | |
| } | |
| # read last selected container for a context/namespace/pod | |
| read_last_container() { | |
| [[ "${use_history}" == "true" ]] || return ; | |
| local ctx="$1" ; | |
| local ns="$2" ; | |
| local pod="$3" ; | |
| jq -r --arg ctx "$ctx" --arg ns "$ns" --arg pod "$pod" '.contexts[$ctx].namespaces[$ns].pods[$pod].last_container // empty' "${history_file}" ; | |
| } | |
| # write last selected context | |
| write_last_context() { | |
| local ctx="$1" ; | |
| jq --arg ctx "$ctx" '.last_context = $ctx' "${history_file}" | sponge "${history_file}" ; | |
| } | |
| # write last selected namespace for a context | |
| write_last_namespace() { | |
| local ctx="$1" ; | |
| local ns="$2" ; | |
| jq --arg ctx "$ctx" --arg ns "$ns" '.contexts[$ctx].last_namespace = $ns' "${history_file}" | sponge "${history_file}" ; | |
| } | |
| # write last selected pod for a context/namespace | |
| write_last_pod() { | |
| local ctx="$1" ; | |
| local ns="$2" ; | |
| local pod="$3" ; | |
| jq --arg ctx "$ctx" --arg ns "$ns" --arg pod "$pod" '.contexts[$ctx].namespaces[$ns].last_pod = $pod' "${history_file}" | sponge "${history_file}" ; | |
| } | |
| # write last selected container for a context/namespace/pod | |
| write_last_container() { | |
| local ctx="$1" ; | |
| local ns="$2" ; | |
| local pod="$3" ; | |
| local container="$4" ; | |
| jq --arg ctx "$ctx" --arg ns "$ns" --arg pod "$pod" --arg container "$container" '.contexts[$ctx].namespaces[$ns].pods[$pod].last_container = $container' "${history_file}" | sponge "${history_file}" ; | |
| } | |
| select_from_list() { | |
| label=$(echo "Select a ${1:-item} ..."|lolcat -f) ; | |
| if [[ -n "${2:-}" ]] ; then | |
| fzf --height="~100%" --border --border-label-pos=5:top --border-label="> ${label} <" --query="${2}" ; | |
| else | |
| fzf --height="~100%" --border --border-label-pos=5:top --border-label="> ${label} <" ; | |
| fi | |
| } | |
| select_from_list_or_exit() { | |
| selected_item=$(select_from_list "${1:-item}" "${2:-}") ; | |
| if [[ -z "${selected_item}" ]] ; then | |
| > /dev/tty echo "No ${1:-item} selected, aborting!" ; | |
| /usr/bin/env false ; | |
| else | |
| highlighted_item=$(echo "\"${selected_item}\"" | lolcat -f) ; | |
| > /dev/tty printf "Selected ${1:-item}: %s\n" "${highlighted_item}" ; | |
| echo "${selected_item}" ; | |
| fi | |
| } | |
| export kube_context=""; | |
| export kube_namespace=""; | |
| export kube_pod=""; | |
| export pod_container=""; | |
| context() { | |
| [[ -z "${kube_context}" ]] && kube_context=$( | |
| cache_dir="${cache_home}" ; | |
| cache_file="${cache_dir}/kube_contexts.txt" ; | |
| [[ "${refresh_cache}" == "true" ]] && rm -f "${cache_file}" ; | |
| [[ -f "${cache_file}" ]] || ( | |
| mkdir -p "${cache_dir}" ; | |
| command kubectl config view | \ | |
| yq '.contexts[].name' \ | |
| > "${cache_file}" ; | |
| ) ; | |
| last_context=$(read_last_context) ; | |
| < "${cache_file}" select_from_list_or_exit 'kube context' "${last_context}" ; | |
| ) ; | |
| write_last_context "${kube_context}" ; | |
| echo "${kube_context}" ; | |
| } | |
| namespace() { | |
| > /dev/null context ; # prime the kube context | |
| [[ -z "${kube_namespace}" ]] && kube_namespace=$( | |
| cache_dir="${cache_home}/${kube_context}" ; | |
| cache_file="${cache_dir}/kube_namespaces.txt" ; | |
| [[ "${refresh_cache}" == "true" ]] && rm -f "${cache_file}" ; | |
| [[ -f "${cache_file}" ]] || ( | |
| mkdir -p "${cache_dir}" ; | |
| command kubectl --output=json --context "$(context)" get namespaces | \ | |
| jq -r '.items[].metadata.name' \ | |
| > "${cache_file}" ; | |
| # jq -r '.items[] | select(.status.phase=="Active") | .metadata.name' | |
| ) ; | |
| last_namespace=$(read_last_namespace "${kube_context}") ; | |
| < "${cache_file}" select_from_list_or_exit 'kube namespace' "${last_namespace}" ; | |
| ) ; | |
| write_last_namespace "${kube_context}" "${kube_namespace}" ; | |
| echo "${kube_namespace}" ; | |
| } | |
| pod() { | |
| > /dev/null namespace ; # prime the kube namespace (and context) | |
| [[ -z "${kube_pod}" ]] && kube_pod=$( | |
| cache_dir="${cache_home}/${kube_context}/${kube_namespace}" ; | |
| cache_file="${cache_dir}/kube_pods.txt" ; | |
| [[ "${refresh_cache}" == "true" ]] && rm -f "${cache_file}" ; | |
| [[ -f "${cache_file}" ]] || ( | |
| mkdir -p "${cache_dir}" ; | |
| command kubectl --output=json --context "$(context)" --namespace "$(namespace)" get pods | \ | |
| jq -r '.items[]|.metadata.name' \ | |
| > "${cache_file}" ; | |
| # jq -r '.items[] | select(.status.phase=="Running") | .metadata.name' | |
| ) | |
| last_pod=$(read_last_pod "${kube_context}" "${kube_namespace}") ; | |
| < "${cache_file}" select_from_list_or_exit 'kube pod' "${last_pod}" ; | |
| ) ; | |
| write_last_pod "${kube_context}" "${kube_namespace}" "${kube_pod}" ; | |
| echo "${kube_pod}" ; | |
| } | |
| container() { | |
| > /dev/null pod ; # prime the kube pod (and namespace, and context) | |
| [[ -z "${pod_container}" ]] && pod_container=$( | |
| cache_dir="${cache_home}/${kube_context}/${kube_namespace}/${kube_pod}" ; | |
| cache_file="${cache_dir}/pod_containers.txt" ; | |
| [[ "${refresh_cache}" == "true" ]] && rm -f "${cache_file}" ; | |
| [[ -f "${cache_file}" ]] || ( | |
| mkdir -p "${cache_dir}" ; | |
| command kubectl --output=json --context "$(context)" --namespace "$(namespace)" get pods "$(pod)" | \ | |
| jq -r '.spec.containers[].name' \ | |
| > "${cache_file}" ; | |
| ) | |
| last_container=$(read_last_container "${kube_context}" "${kube_namespace}" "${kube_pod}") ; | |
| < "${cache_file}" select_from_list_or_exit 'pod container' "${last_container}" ; | |
| ) ; | |
| write_last_container "${kube_context}" "${kube_namespace}" "${kube_pod}" "${pod_container}" ; | |
| echo "${pod_container}" ; | |
| } | |
| kubectl_cmd="$1" ; shift ; | |
| case "${kubectl_cmd}" in | |
| "exec") | |
| > /dev/null container ; # prime the pod container | |
| [[ $# -eq 0 ]] && ( | |
| command kubectl --context "$(context)" exec -it --namespace "$(namespace)" "$(pod)" --container "$(container)" -- /bin/sh ; | |
| ) | |
| [[ $# -ne 0 ]] && ( | |
| command kubectl --context "$(context)" exec -it --namespace "$(namespace)" "$(pod)" --container "$(container)" -- /bin/sh -c "$*" ; | |
| ) | |
| ;; | |
| "env") | |
| > /dev/null container ; # prime the pod container | |
| command kubectl --context "$(context)" exec -it --namespace "$(namespace)" "$(pod)" --container "$(container)" -- /usr/bin/env ; | |
| ;; | |
| "logs") | |
| > /dev/null container ; # prime the pod container | |
| command kubectl --context "$(context)" logs "$@" --namespace "$(namespace)" "$(pod)" --container "$(container)" ; | |
| ;; | |
| "pods") | |
| > /dev/null namespace ; # prime the kube namespace | |
| command kubectl --context "$(context)" get pods -o wide --namespace "$(namespace)" ; | |
| ;; | |
| "port-forward") | |
| > /dev/null pod ; # prime the kube pod | |
| command kubectl --context "$(context)" --namespace "$(namespace)" port-forward pod/"$(pod)" "$@" ; | |
| ;; | |
| "top") | |
| > /dev/null namespace ; # prime the kube namespace | |
| command kubectl --context "$(context)" top pod --namespace "$(namespace)" --containers ; | |
| ;; | |
| "delete") | |
| > /dev/null pod ; # prime the kube pod | |
| command kubectl --context "$(context)" delete pod --namespace "$(namespace)" "$(pod)" --now --interactive ; | |
| ;; | |
| *) | |
| > /dev/null context ; # prime the kube context | |
| command kubectl --context "$(context)" "$@" ; | |
| ;; | |
| esac | |
| # That's all Folks! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment