kls/kls

184 lines
9.9 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
import curses, subprocess
screen = curses.initscr() # инициализация экрана
screen.refresh() # не знаю зачем это нужно но без этого не работает
curses.set_escdelay(1) # в curses зачем-то сделали задержку на срабатывание Escape, уменьшаем её до 1 милисекунды (до 0 нельзя)
screen.keypad(True) # нужно для работы с клавишами F1-F4
curses.curs_set(0) # делаем курсор невидимым
curses.noecho() # не выводим символы вверху
SELECTED_WITHOUT_SEARCH, SELECTED_WITH_SEARCH, NOT_SELECTED_WITHOUT_SEARCH, NOT_SELECTED_WITH_SEARCH = 1, 2, 3, 4 # состояния меню
STATE = "running" # состояние программы
HELP_WIN = curses.newwin(3, curses.COLS, curses.LINES - 3, 0) # окно помощи
class Menu:
def __init__(self, name, rows, begin_x, width, state):
self.state = state # состояние меню
self.name = name # заголовок меню
self.rows = rows # строки меню
self.filter = "" # фильтр строк меню
self.selected_row = 0 # выбранная строка меню
self.begin_x = begin_x # где начинается меню по х?
self.win = curses.newwin(curses.LINES - 3, width, 0, begin_x) # окно с высотой во весь экран, шириной width, и началом по х в точке begin_x
self.rows_number = curses.LINES - 10 # максимальное число видимых строк меню, начиная с 0
@property
def filtered_rows(self):
return [x for x in self.rows if self.filter in x]
def execute_cmd(command):
return subprocess.check_output(command, shell=True).decode().strip().split()
# инициализируем меню
namespaces = execute_cmd("kubectl get ns --no-headers | awk '{print $1}'")
api_resources_top = ["pods", "services", "deployments", "statefulsets", "daemonsets", "ingresses", "configmaps", "secrets", "persistentvolumes", "persistentvolumeclaims", "nodes", "storageclasses"]
api_resources_kubectl = execute_cmd("kubectl api-resources --no-headers --verbs=get | awk '{print $1}'")
api_resources = api_resources_top + sorted(list(set(api_resources_kubectl) - set(api_resources_top)))
pods = execute_cmd(f"kubectl get pods --no-headers -n {namespaces[0]} | awk '{{print $1}}'")
menus = [
Menu("Namespaces", namespaces, 0, curses.COLS // 10 * 2, 1),
Menu("API resources", api_resources, curses.COLS // 10 * 2, curses.COLS // 10 * 3, 3),
Menu("Resources", pods, curses.COLS // 10 * 5, curses.COLS - curses.COLS // 10 * 5, 3)
]
def update_menu3_object():
if not menus[0].filtered_rows or not menus[1].filtered_rows:
menus[2].rows = ["No resources matched criteria.", ]
else:
namespace = menus[0].filtered_rows[menus[0].selected_row]
api_resource = menus[1].filtered_rows[menus[1].selected_row]
menus[2].rows = execute_cmd(f"kubectl get {api_resource} --no-headers -n {namespace} | awk '{{print $1}}'")
if not menus[2].rows: menus[2].rows = [f"No resources found in {namespace} namespace.", ]
menus[2].selected_row = 0
def draw_header(menu):
menu.win.addstr(1, 2, menu.name, curses.A_REVERSE | curses.A_BOLD if menu.state in [1, 2] else curses.A_NORMAL)
menu.win.refresh()
def draw_rows(menu):
if not menu.filtered_rows: return # если строк нет, рисовать нечего
# ограничиваем число отфильтрованных строк высотой окна + выбираем, от какой cтроки меню будет начинаться меню
first_row_index = 0 if menu.selected_row < menu.rows_number else menu.selected_row - menu.rows_number + 1
last_row_index = first_row_index + menu.rows_number
filtered_rows = menu.filtered_rows[first_row_index:last_row_index]
selected_row_index = menu.selected_row - first_row_index # индекс выбранной строки в отфильтрованных строках
# if menus[1].selected_row != 0: # debug
# raise ValueError(f"{len(filtered_rows)} {selected_row_index} {first_row_index} {last_row_index} {menu.rows_number}")
for index, row in enumerate(filtered_rows): # рисуем то, что отфильтровали
menu.win.addstr(index + 3, 2, row, curses.A_REVERSE | curses.A_BOLD if index == selected_row_index else curses.A_NORMAL)
menu.win.box()
menu.win.refresh()
def draw_search_box(menu):
menu.win.addstr(curses.LINES - 5, 2, f"/{menu.filter}" if menu.state in [2, 4] else "Press / for search") # рисуем контент
menu.win.clrtoeol() # очищаем остальную часть строки
menu.win.box() # рисуем рамку
menu.win.refresh() # обновляем окно
def draw_menu(menu):
menu.win.clear() # очищаем окно меню
draw_header(menu) # рисуем заголовок
draw_rows(menu) # рисуем строки меню
draw_search_box(menu) # рисуем строку поиска
def draw_windows():
for menu in menus:
draw_menu(menu)
help_text = "q: exit, /: enter search mode, Esc: exit search mode, F1: get yaml, F2: describe, F3: edit, F4: pod logs"
HELP_WIN.addstr(1, 2, help_text)
HELP_WIN.box() # рисуем рамку
HELP_WIN.refresh() # обновляем окно
def run_command(key_pressed):
if not menus[2].filtered_rows or menus[2].filtered_rows[0].startswith("No resources"): return # если пусто, выходим
api_resource = menus[1].filtered_rows[menus[1].selected_row]
if key_pressed == "KEY_F(4)" and api_resource != "pods": return # логи можно посмотреть только у подов
namespace = menus[0].filtered_rows[menus[0].selected_row]
resource = menus[2].rows[menus[2].selected_row]
commands = {
"KEY_F(1)": f'kubectl -n {namespace} get {api_resource} {resource} -o yaml | batcat -l yaml --paging always --style numbers',
"KEY_F(2)": f'kubectl -n {namespace} describe {api_resource} {resource} | batcat -l yaml --paging always --style numbers',
"KEY_F(3)": f'kubectl edit {api_resource} -n {namespace} {resource}',
"KEY_F(4)": f'kubectl -n {namespace} logs {resource} | batcat -l log --paging always --style numbers'
}
curses.def_shell_mode()
subprocess.call(commands[key_pressed], shell=True)
curses.reset_shell_mode()
draw_windows()
def navigate_horizontally(key_pressed, menu):
increment = {"KEY_RIGHT": 1, "\t": 1, "KEY_LEFT": -1, "KEY_BTAB": -1}
next_menu = menus[(menus.index(menu) + increment[key_pressed]) % 3]
menu.state = NOT_SELECTED_WITH_SEARCH if menu.filter else NOT_SELECTED_WITHOUT_SEARCH
next_menu.state = SELECTED_WITH_SEARCH if next_menu.filter else SELECTED_WITHOUT_SEARCH
draw_header(menu) # убираем выделение с заголовка текущего меню
draw_header(next_menu) # выделяем заголовок следующего/предыдущего меню
def navigate_vertically(key_pressed, menu):
if not menu.filtered_rows or len(menu.filtered_rows) == 1: return # если строк нет или строка одна, навигация не нужна
increment = {"KEY_DOWN": 1, "KEY_UP": -1}
menu.selected_row = (menu.selected_row + increment[key_pressed]) % len(menu.filtered_rows) # выбираем строку учитывая сколько строк в меню
if menu != menus[2]: # перерисовываем третье меню, т. к. оно зависит от вертикальной навигации в меню 1 и 2
update_menu3_object()
draw_menu(menus[2])
draw_menu(menu) # перерисовываем меню
def handle_selected_with_search_state(key_pressed, menu):
if key_pressed == "\x1b": # Escape key exits search mode
menu.filter = ""
menu.selected_row = 0
menu.state = SELECTED_WITHOUT_SEARCH
elif key_pressed == "KEY_BACKSPACE":
menu.state = SELECTED_WITHOUT_SEARCH if not menu.filter else menu.state
menu.filter = menu.filter[:-1] if menu.filter else ""
elif key_pressed.isalpha() or key_pressed.isnumeric() or key_pressed == "-":
menu.filter += key_pressed
menu.selected_row = 0
else:
return
if menu != menus[2]: # перерисовываем третье меню, т. к. оно зависит от нажатия Esc/Backspace/etc. в меню 1 и 2
update_menu3_object()
draw_menu(menus[2])
draw_menu(menu)
def handle_selected_without_search_state(key_pressed, menu):
if key_pressed == "/":
menu.state = SELECTED_WITH_SEARCH
draw_search_box(menu)
elif key_pressed == "q":
global STATE
STATE = "interrupt"
def catch_input(menu):
key_pressed = screen.getkey()
if key_pressed in ["\t", "KEY_RIGHT", "KEY_BTAB", "KEY_LEFT"]: navigate_horizontally(key_pressed, menu)
elif key_pressed in ["KEY_DOWN", "KEY_UP"]: navigate_vertically(key_pressed, menu)
elif key_pressed in ["KEY_F(1)", "KEY_F(2)", "KEY_F(3)", "KEY_F(4)"]: run_command(key_pressed)
elif menu.state == SELECTED_WITH_SEARCH: handle_selected_with_search_state(key_pressed, menu)
elif menu.state == SELECTED_WITHOUT_SEARCH: handle_selected_without_search_state(key_pressed, menu)
def main():
draw_windows() # рисуем начальный экран
while STATE == "running":
for menu in menus:
if menu.state in [1, 2]: catch_input(menu) # если меню выбрано, перехватываем ввод пользователя
main()
subprocess.call(["reset"]) # Потому что терминал не работает без этого после выхода из kls