kls/kls

182 lines
9.7 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" # состояние программы
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, width, 0, begin_x) # окно с высотой во весь экран, шириной width, и началом по х в точке begin_x
self.rows_number = curses.LINES - 10 # максимальное число видимых строк меню, начиная с 0
@property
def filtered_rows(self):
return list(filter(lambda x: (self.filter in x), self.rows)) # фильтрованные строки меню
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 - 2, 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_window():
for menu in menus:
draw_menu(menu)
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_window()
def navigate_horizontally(direction, menu):
increment = {"right": 1, "left": -1}
next_menu = menus[(menus.index(menu) + increment[direction]) % 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(direction, menu):
if not menu.filtered_rows or len(menu.filtered_rows) == 1: return # если строк нет или строка одна, навигация не нужна
increment = {"down": 1, "up": -1}
menu.selected_row = (menu.selected_row + increment[direction]) % 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 == '\t' or key_pressed == "KEY_RIGHT": navigate_horizontally("right", menu)
elif key_pressed == "KEY_BTAB" or key_pressed == "KEY_LEFT": navigate_horizontally("left", menu)
elif key_pressed == "KEY_DOWN": navigate_vertically("down", menu)
elif key_pressed == "KEY_UP": navigate_vertically("up", 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_window() # рисуем начальный экран
while STATE == "running":
for menu in menus:
if menu.state in [1, 2]: catch_input(menu) # если меню выбрано, перехватываем ввод пользователя
main()
subprocess.call(["reset"]) # Потому что терминал не работает без этого после выхода из kls