Skip to content

Instantly share code, notes, and snippets.

@pvdb
Last active January 4, 2026 08:47
Show Gist options
  • Select an option

  • Save pvdb/813b5ba5bb1383c526d9dbcde64c9878 to your computer and use it in GitHub Desktop.

Select an option

Save pvdb/813b5ba5bb1383c526d9dbcde64c9878 to your computer and use it in GitHub Desktop.
kubectl wrapper
#!/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