
Всем привет, меня зовут Антон и я люблю tmux хлоп-хлоп-хлоп.
Для меня tmux - это не просто терминальный мультиплексор, это центр моей консоли: чтобы я не намеревался сделать - открытие новой панели будет моей стартовой точкой, особенно когда дело касается Kubernetes.
И вот однажды, в момент очередного использования kubectl config current-context появилась идея - "а не научить ли tmux показывать мой контекст?"
В этой практической статье мы с вами по шагам воплотим эту идею в код - напишем простой плагин для отображения текущего контекста kubernetes, и ровно так, как этот плагин вырос из совершенного другого, возможно кто-то захочет переиспользовать предложенную реализацию для своих Quality of Life плагинов.
Предыстория и как это вообще работает
Идея этого плагина выросла из другого - tmux-current-pane-hostname - маленький плагин, отображающий имя пользователя и хоста при ssh соединении в активной панели. Сейчас же я хочу написать плагин для удобства работы с Kubernetes - на этом примере и посмотрим в общих чертах как плагины пишутся, устанавливаются и работают.
Есть (AFAIK) 2 основных способа подключения плагинов: вручную и через tpm. Оба способа очень (вплоть до идентичности) похожи на установку плагинов для zsh:
вручную - клонируем плагин в любую удобную директорию и подключаем в tmux.conf , например
run-shell ~/.config/tmux/plugins/tmux-plugin-name/plugin_name.tmuxчерез tpm - добавляем
set -g @plugin "acme/tmux-plugin-name"в tmux.conf, а дальше вызываем установку сочетанием prefix+I. Tpm сам с��лонирует репозиторий с плагином в"${xdg_tmux_path:-~/.tmux}/plugins/", и подключит все *.tmux файлы.
Исходя из этого и документации о том как создавать плагины получаем нашу точку входа, с которой и начнем писать наш плагин - файл *.tmux.
Начало
Точка входа
Для начала создадим рыбу плагина и добавим его в tmux.conf:
Прим.: здесь все пути соответствуют моей конфигурации (~/.config/tmux/plugins/), если вы предпочитаете другое расположение конфига/плагинов, пример ~/.tmux.conf, ~/.tmux/plugins/ - учитывайте это.
cd $XDG_CONFIG_HOME/tmux/plugins
mkdir tmux-kubectx && cd $_
touch kubectx.tmux && chmod +x $_
# Здесь намерено упущено создание README.md, LICENSE и прочего,
# так как это не важно в данном контексте
echo 'run-shell "${XDG_CONFIG_HOME}/tmux/plugins/tmux-kubectx/kubectx.tmux"
' >> $XDG_CONFIG_HOME/tmux/tmux.confЗапустим tmux и перейдем к самой разработке.
Hello world
Начнем с основ - заставим наш плагин выводить "Hello world" в статус-панель:
#!/usr/bin/env bash
main() {
echo "Hello world" > /dev/pts/3
}
main
Прим.: здесь /dev/pts/3 - tty другой панели tmux, мы можем направлять поток данных в tty для отладки того что происходит в скрипте (и не только в скрипте на самом деле - это очень крутая особенность работы с потоками в linux)
Итак мы увидели "Hello world", а значит на верном пути - осталось сделать так, чтобы текст был в нужном нам месте, но тут есть свой нюанс.
У tmux есть возможность встраивания скриптов в значение определенных параметров с помощью конструкции #() (более подробно об этом конечно же написано в man), которые будут исполняться при каждом обновлении статус-панели и именно эту возможность мы будем использовать - это позволит нам переложить ответственность за частоту обновления и отрисовку на сам tmux.
Теперь попробуем применить это знание, сперва добавив параметр в статус-панель справа:
set-option -g status-right "#{?#{kubectx_full}, #{kubectx_full} ,}#{tmux_mode_indicator} %T %d %B %Y "Прим.: #{tmux_mode_indicator} - плагин для осображения текущего режима, а %T %d %B %Y всего лишь показ даты и времени.
А затем, научим плагин работать с параметрами.
Взаимодействие с параметрами tmux
Для чистоты кода создам простенькую файловую структуру и ключевые функции по взаимодействию с параметрами.
Сперва создадим скрипт для встраивания scripts/all.sh (спойлер: он будет не единственным) в котором разместим вывод "Hello world", но не только в панель для отладки, но и в STDOUT.
#!/usr/bin/env bash
main() {
echo "Hello world" | tee /dev/pts/3
}
mainВажно, что встраиваемые файлы должны быть исполняемыми, поэтому не забываем про chmod +x
Затем создадим отдельный файл для работы с самим tmux - scripts/utils/tmux.sh, над содержим которого нужно будет остановиться поподробнее:
#!/usr/bin/env bash
get_tmux_option() {
local option=$1
local default_value=$2
local option_value=$(tmux show-option -gqv "$option")
[[ -z "$option_value" ]] && echo "$default_value" || echo "$option_value"
}
set_tmux_option() {
local option=$1
local value=$2
tmux set-option -gq "$option" "$value"
}Здесь мы будем пользоваться тем, что tmux позволяет с помощью CLI обратиться к серверу (да-да, tmux работает по клиент-серверной архитектуре, подробнее об этом конечно же в man) для получения и, что гораздо интереснее, установки параметров. Обратить внимание здесь стоит на флаги команд tmux (show|set)-option:
-g- show global options-q- suppress errors about unknown or ambiguous options-v- show only the option value, not the name
Так, мы сможем организовать обмен данным между плагином и tmux - получая, изменяя и устанавливая значения параметров.
И изменим наш kubectx.tmux в соответствии с новой логикой:
#!/usr/bin/env bash
CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
source "${CURRENT_DIR}/scripts/utils/tmux.sh"
main() {
local option option_value new_option_value
local placeholder='#{kubectx_full}'
local script="#(${CURRENT_DIR}/scripts/all.sh)"
for option in "status-right" "status-left" "pane-border-format"; do
option_value=$(get_tmux_option "$option")
new_option_value="${option_value//$placeholder/$script}"
set_tmux_option "$option" "$new_option_value"
done;
}
mainЗдесь мы должны сразу учитывать использование параметра #{kubectx_full} во всех местах его применения, где хотим поддерживать - status-right, status-left и pane-border-format.
В результате получаем нужный нам вывод как в панель отладки /dev/pts/3 так и в статус-панель:

При этом видим, что сообщения в панель отладки повторяются, что говорит о том, что наш all.sh выполняется в соответствии с установленным значением обновления status-interval - еще один шаг к цели пройден.
Набор массы
Взаимодействие к конфигурацией Kubernetes
Теперь когда мы научились общаться с tmux и умеем встраивать скрипты, самое время перейти к получению нужной информации о kubernetes.
Работа с k8s чаще всего происходит через kubectl - это CLI утилита для взаимодействие с API k8s, она использует файл конфигурации, в котором содержит информацию о знакомых контекстах, кластерах и пользователях, например:
apiVersion: v1
clusters:
- cluster:
certificate-authority: raspberry.pem
server: https://raspberry-01:6443
name: raspberry
contexts:
- context:
cluster: raspberry
namespace: default
user: raspberry-admin
name: admin@raspberry
current-context: admin@raspberry
kind: Config
users:
- name: raspberry-admin
user:
client-certificate: raspberry/admin.pem
client-key: raspberry/admin.keyИ именно отсюда мы будем брать нужные нам данные с помощью kubectl config view. Однако нам нужно ограничиться только текущим контекстом, с чем поможет флаг --minify, а так же только определенными полями: контекстом, кластером, пользователем и неймспейсом, с чем поможет go-template:
kubectl config view --minify --flatten --output go-template='
{{- with (index .contexts 0) -}}
{{- .name }} {{ .context.cluster }} {{ or .context.namespace "default" }} {{ .context.user -}}
{{- end -}}'Разберем этот шаблон:
contextsвсегда является списком объектовcontext, но т.к. мы используем--minify,context.nameнулевого элемента всегда будет равенcurrent-contextcontextможет не содержать поляnamespace- в этом том случае kubectl будет использовать имя неймспейса по умолчанию -defaultиспользуя контекст
contexts[0]вытаскиваем имя контекста, имя кластера, неймспейс (либо "default"), имя пользователя и агрегируем в итоговую строку
Создадим новый файл scripts/utils/kube.sh в котором инкапсулируем нужную функцию:
#!/usr/bin/env bash
get_info() {
local info=$(command kubectl config view --minify --flatten --output go-template='
{{- with (index .contexts 0) -}}
{{- .name }} {{ .context.cluster }} {{ or .context.namespace "default" }} {{ .context.user -}}
{{- end -}}' 2>/dev/null)
echo "$info"
}
И используем ее в all.sh:
#!/usr/bin/env bash
CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
source "${CURRENT_DIR}/utils/kube.sh"
main() {
get_info | tee /dev/pts/3
}
mainВ результате мы увидим то что нужно - контекст, имя кластера, неймспейс и имя пользователя в статус-панели.

Развиваем идею
Наш MVP с огромной натяжкой можно считать готовым, но мы же хотим сделать все красиво, а значит продолжаем.
Первое что хочется реализовать, это разделить вывод: дать возможность пользователю самому решать, какая часть информации ему нужна - только неймспейс, или только имя контекста, а может пользователь хочет имя кластера в статус-панели, а имя пользователя в заголовке панели.
Для этого поправим функцию get_info в scripts/utils/kube.sh, чтобы она стала чуть умнее:
#!/usr/bin/env bash
get_info() {
local context cluster namespace user
IFS='#' read -r context cluster namespace user <<<"$(command kubectl config view \
--minify \
--flatten \
--output go-template='
{{- with (index .contexts 0) -}}
{{- .name }}#{{ .context.cluster }}#{{ or .context.namespace "default" }}#{{ .context.user -}}
{{- end -}}' 2>/dev/null)"
[[ -z "$context" ]] && return
case "$1" in
"context")
echo "$context"
;;
"cluster")
echo "$cluster"
;;
"namespace")
echo "$namespace"
;;
"user")
echo "$user"
;;
"")
echo "$context $cluster $namespace $user"
;;
esac
}Так, мы теперь можем получать как что-то определенное, например имя контекста, так и всю строку сразу. Здесь важно, что так как какое-либо из полей может содержать пробел - используем # в качестве разделителя (# выбран потому, что с него начинается комментарий как в yaml, так и в bash, а значит не будет коллизий при дальнейшей обработке). После, рядом с all.sh создадим новые файлы, каждый из которых будет отвечать за свой компонент - сначала просто скопируем сам all.sh:
for f in cluster.sh context.sh namespace.sh user.sh; do cp all.sh $f; doneА затем передадим нужные аргументы в get_info в каждом новом файле:

А что же tmux
Мы создали нужные скрипты, самое время причесать взаимодействие с tmux, а именно обеспечить возможность их встраивания.
Дабы не плодить хард-код зададим наши параметры в начале scripts/utils/tmux.sh:
declare -r kubectx_full='#{kubectx_full}'
declare -r kubectx_context='#{kubectx_context}'
declare -r kubectx_cluster='#{kubectx_cluster}'
declare -r kubectx_namespace='#{kubectx_namespace}'
declare -r kubectx_user='#{kubectx_user}'И научим kubectx.tmux их корректно подставлять:
#!/usr/bin/env bash
CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
source "${CURRENT_DIR}/scripts/utils/tmux.sh"
# Create map placeholders to handle scripts
# to provide one handler for multiple placeholders.
# Use '//' as separator, due to unix limitation in filenames
placeholders_to_scripts=(
"$kubectx_full//#(${CURRENT_DIR}/scripts/all.sh)"
"$kubectx_context//#(${CURRENT_DIR}/scripts/context.sh)"
"$kubectx_cluster//#(${CURRENT_DIR}/scripts/cluster.sh)"
"$kubectx_namespace//#(${CURRENT_DIR}/scripts/namespace.sh)"
"$kubectx_user//#(${CURRENT_DIR}/scripts/user.sh)")
do_interpolation() {
local interpolated=$1
for assignment in ${placeholders_to_scripts[@]}; do
# ${assignment%\/\/*} - remove from // til EOL
local placeholder="${assignment%\/\/*}"
# ${assignment#*\/\/} - remove from BOL til //
local script="${assignment#*\/\/}"
# ${interpolated//A/B} - replace all occurrences of A in interpolated with B
interpolated="${interpolated//${placeholder}/${script//[[:space:]]/\\ }}"
done
echo "$interpolated"
}
main() {
local option option_value new_option_value
for option in "status-right" "status-left" "pane-border-format"; do
option_value=$(get_tmux_option "$option")
new_option_value=$(do_interpolation "$option_value")
set_tmux_option "$option" "$new_option_value"
done;
}
mainО - оптимизация
На текущем этапе уже мы можем собрать итоговую строку по своему желанию из имеющихся плейсхолдеров, однако данный путь не является оптимальным.
Как мы помним, tmux с определенным интервалом перерисовывает статус-панель и вызывает все встроенные скрипты, при этом на ~99% они идентичны: обратиться к kubectl, получить данные, вытащить нужное и отдать - так почему бы не сделать возможным получение всего и сразу, для чего у нас как раз all.sh простаивает, но при этом красиво и удобно? Давайте сделаем!
Для начала добавим параметр в tmux, в котором будем хранить шаблон того, как хотим видеть данные. Для этого добавим в начало scritps/utils/tmux.sh объявление параметра:
declare -r kubectx_full_format='@kubectx-format'В tmux.conf его инициализацию:
set -g @kubectx-format "%{context}:%{namespace}"А в all.sh его использование:
#!/usr/bin/env bash
CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
source "${CURRENT_DIR}/utils/kube.sh"
source "${CURRENT_DIR}/utils/tmux.sh"
main() {
local context cluster namespace user
IFS='#' read -r context cluster namespace user <<<"$(get_info)"
# empty context means, that `kubectl config current-context` returns
# "error: current-context is not set"
[[ -z $context ]] && return
get_tmux_option $kubectx_full_format | sed \
-e "s/%{context}/$context/" \
-e "s/%{cluster}/$cluster/" \
-e "s/%{namespace}/$namespace/" \
-e "s/%{user}/$user/"
}
mainОбратите внимание, что здесь мы снова используем # в качестве разделителя полей, а значит get_info в scripts/utils/kube.sh для кейса "" так же нужно заменить пробелы на разделитель:
- "")
- echo "$context $cluster $namespace $user"
- ;;
+ "")
+ echo "$context#$cluster#$namespace#$user"
+ ;;В результате мы можем использовать только #{kubectx_full} для встраивания и @kubectx-format для форматирования, оставив context.sh, cluster.sh, namespace.sh и user.sh для, например, размещения в статус-панели слева.

Внезапный контекст
Долгое время я пользовался плагином в таком виде и меня все устраивало - ровно до момента разделения конфигурации kubectl по свои проектам и отказу от глобального KUBECONFIG, из-за чего контекст перестал отображаться. Что же пошло не так - давайте разбираться.
KUBECONFIG
Согласно документации, переменная окружения KUBECONFIG содержит не просто путь до файла - она может содержать в себе несколько путей, подобно PATH, используя : в качестве разделителя. На этом (а так же на магии mise [или direnv, или chpwd]) строилось мое разделение KUBECONFIG:

Но страиваемые скрипты ничего не знают про контекст проекта, по простому - они не знают в какой директории я в данный момент и какой актуальный KUBECONFIG. Надо это исправить.
Снова tmux
У tmux есть встроенная переменная #{pane_current_path}, в которой лежит актуальное значение $PWD для текущей панели, так что нужно всего лишь вытащить оттуда значение и передать в get_info, но тут есть нюанс:
[[ -z $(tmux show-option -gqv "#{pane_current_path}") ]] && echo 'empty'
empty#{pane_current_path} с точки зрения tmux не option, а variable, а значит нам нужна новая функция в scripts/utils/tmux.sh
declare -r pane_current_path='#{pane_current_path}'
get_tmux_message() {
local option=$1
local default_value=$2
local option_value=$(tmux display-message -pF "$option")
[[ -z "$option_value" ]] && echo "$default_value" || echo "$option_value"
}Прим.: буду рад советам по красивому объединению get_tmux_message с get_tmux_option в комментариях
Простым путем было бы пойти в scripts/utils/kube.sh и использовать get_tmux_message там, но более правильным кажется не создавать лишнюю связь kube<-tmux, а пойти через уже имеющееся - файлы all.sh, context.sh и т.д.
args vs env
Внутри scripts/utils/kube.sh наша функция get_info уже имеет один параметр и, с точки зрения соблюдения чистоты функций, было бы правильно добавить еще один параметр, но мы пойдем другим путем - через подмену и использование $PWD.
Суть довольно проста - bash (а так же zsh и, наверное, другие сходные оболочки) при переходе по файловой системе сохраняют текущий актуальный путь в переменную PWD, а мы в свою очередь переопределим ее на нужный нам путь во встраиваемых скриптах, а потом считаем его уже непосредственно в get_info.
Отредактируем scripts/utils/kube.sh:
- IFS='#' read -r context cluster namespace user <<<"$(command kubectl config view \
+ IFS='#' read -r context cluster namespace user <<<"$(cd $PWD && command kubectl config view \А так же встраиваемые скрипты, на примере scripts/context.sh:
source "${CURRENT_DIR}/utils/kube.sh"
+ source "${CURRENT_DIR}/utils/tmux.sh"
...
- get_info context
+ PWD=$(get_tmux_message $pane_current_path) get_info contextИ scripts/all.sh:
- IFS='#' read -r context cluster namespace user <<<"$(get_info)"
+ IFS='#' read -r context cluster namespace user <<<"$(PWD=$(get_tmux_message $pane_current_path) get_info)"В итоге все работает так, как нужно:

Заключение
Таким образом мы получили простой, но работающий плагин, немного облегчающий мне работу с k8s. А еще эту статью-туториал, которая как надеюсь замотивирует еще кого-нибудь на подобные приключения.
Делитесь похожими штуками в комментариях, пользуйтесь классными инструментами, избавляйтесь от рутины и придумывайте новое - это весело и увлекательно.