Listing pods with kubectl get pods
, then select a pod name and copy paste it into kubectl logs [pod name]
- I want to streamline my workflow and stop using the terminal
- learn more about kubernetes
- main kubernetes extension for Emacs out there is greedy for permissions
- assimilate! assimilate! assimilate!
- making a major mode
- interacting with sub-processes
- providing a modern UX
- get pods from shell command with naive processing
- dump that list in an appropriate major mode
- derive our own major mode
kubectl get pods
Only keep pod name and discard first line
kubectl get pods --no-headers=true | awk '{print $1}'
Using the shell-command-to-string
function
(shell-command-to-string "kubectl get pods --no-headers=true | awk '{print $1}'")
Just split at every new line with split-string
function
(split-string (shell-command-to-string "kubectl get pods --no-headers=true | awk '{print $1}'") "\n")
There’s already a great major to display columns of data: tabulated-list-mode
.
The column format as a vector of (name width)
elements where:
name
is the column namewidth
is the column width[("Col1" 50) ("Col2" 50)]
The row entries as a list of '(id [values....])
where each element is a row where:
id
can be left nil or be a unique id for the row[values...]
is a vector of row values(list '(nil ["row1" "value1"]) '(nil ["row2" "value2"]) '(nil ["row3" "value3"]))
(let ((columns [("Col1" 50) ("Col2" 50)])
(rows (list '(nil ["row1" "value1"])
'(nil ["row2" "value2"])
'(nil ["row3" "value3"]))))
(switch-to-buffer "*temp*")
(setq tabulated-list-format columns)
(setq tabulated-list-entries rows)
(tabulated-list-init-header)
(tabulated-list-print))
Set up only one column for pod name.
(let ((columns [("Pod" 100)])
(rows (mapcar (lambda (x) `(nil [,x]))
(split-string (shell-command-to-string "kubectl get pods --no-headers=true | awk '{print $1}'") "\n"))))
(switch-to-buffer "*temp*")
(setq tabulated-list-format columns)
(setq tabulated-list-entries rows)
(tabulated-list-init-header)
(tabulated-list-print))
(define-derived-mode kubernetes-mode tabulated-list-mode "Kubernetes"
"Kubernetes mode"
(let ((columns [("Pod" 100)])
(rows (mapcar (lambda (x) `(nil [,x]))
(split-string (shell-command-to-string
"kubectl get pods --no-headers=true | awk '{print $1}'") "\n"))))
(setq tabulated-list-format columns)
(setq tabulated-list-entries rows)
(tabulated-list-init-header)
(tabulated-list-print)))
(defun kubernetes ()
(interactive)
(switch-to-buffer "*kubernetes*")
(kubernetes-mode))
Can summon with M-x kubernetes
or
(kubernetes)
- async sub-process creation -> no hanging Emacs
- redirect output to a buffer
kubectl logs redis-64896b74dc-zrw7w
Not async and meh performance
Use the call-process
function and direct it to a buffer
(let ((buffer "*kubectl-logs*"))
(call-process "kubectl" nil buffer nil "logs" "redis-64896b74dc-zrw7w")
(switch-to-buffer buffer))
Use the start-process
function instead which will create a process for you
(let ((process "*kubectl*")
(buffer "*kubectl-logs*"))
(start-process process buffer "kubectl" "logs" "-f""redis-64896b74dc-zrw7w")
(switch-to-buffer buffer))
Let’s use the optional arg
(defun kubernetes-get-logs (&optional arg)
(interactive "P")
(let ((process "*kubectl*")
(buffer "*kubectl-logs*"))
(if arg
(start-process process buffer "kubectl" "logs" "-f" "redis-64896b74dc-zrw7w")
(call-process "kubectl" nil buffer nil "logs" "redis-64896b74dc-zrw7w"))
(switch-to-buffer buffer)))
Try it with M-x kubernetes-get-logs
or C-u M-x kubernetes-get-logs
Our major mode is derived from tabulated-list-mode
so we can use the function tabulated-list-get-entry
which will give us the entry under the cursor as a vector:
(aref (tabulated-list-get-entry) 0)
Putting everything together
(defun kubernetes-get-logs (&optional arg)
(interactive "P")
(let ((process "*kubectl*")
(buffer "*kubectl-logs*")
(pod (aref (tabulated-list-get-entry) 0)))
(if arg
(start-process process buffer "kubectl" "logs" "-f" pod)
(call-process "kubectl" nil buffer nil "logs" pod))
(switch-to-buffer buffer)))
Call kubernetes mode with M-x kubernetes
and then look at the logs of pod under cursor with M-x kubernetes-get-logs
- a meaningful UI for users to interact with our major modes
- transient (from the magit project) is perfect for wrapping CLI tools
(defun test-function ()
(interactive)
(message "Test function"))
(define-transient-command test-transient ()
"Test Transient Title"
["Actions"
("a" "Action a" test-function)
("s" "Action s" test-function)
("d" "Action d" test-function)])
(test-transient)
We can easily define command line switches in our transient interface.
(defun test-function (&optional args)
(interactive
(list (transient-args 'test-transient)))
(message "args: %s" args))
(define-transient-command test-transient ()
"Test Transient Title"
["Arguments"
("-s" "Switch" "--switch")
("-a" "Another switch" "--another")]
["Actions"
("d" "Action d" test-function)])
(test-transient)
A bit more complex than simple switches, params let users enter a value.
(defun test-function (&optional args)
(interactive
(list (transient-args 'test-transient)))
(message "args %s" args))
(define-infix-argument test-transient:--message ()
:description "Message"
:class 'transient-option
:shortarg "-m"
:argument "--message=")
(define-transient-command test-transient ()
"Test Transient Title"
["Arguments"
("-s" "Switch" "--switch")
("-a" "Another switch" "--another")
(test-transient:--message)]
["Actions"
("d" "Action d" test-function)])
(test-transient)
After some feedback, I wanted to share that it is simpler and better here to not define the infix argument separately. Instead, the transient could be defined this way and have the same effect
(define-transient-command test-transient ()
"Test Transient Title"
["Arguments"
("-s" "Switch" "--switch")
("-a" "Another switch" "--another")
("-m" "Message" "--message=")] ;; simpler
["Actions"
("d" "Action d" test-function)])
- can just get logs
- can follow logs with
-f
- can specify tail length
--tail=100
- can combine these options
(define-infix-argument kubernetes-transient:--tail ()
:description "Tail"
:class 'transient-option
:shortarg "-t"
:argument "--tail=")
(define-transient-command kubernetes-transient ()
"Test Transient Title"
["Arguments"
("-f" "Follow" "-f")
(kubernetes-transient:--tail)]
["Actions"
("l" "Log" kubernetes-get-logs)])
(kubernetes-transient)
- read args from transient
- check if
-f
is in args to do async or not - pass the args into the process functions
(defun kubernetes-get-logs (&optional args)
(interactive
(list (transient-args 'kubernetes-transient)))
(let ((process "*kubectl*")
(buffer "*kubectl-logs*")
(pod (aref (tabulated-list-get-entry) 0)))
(if (member "-f" args)
(apply #'start-process process buffer "kubectl" "logs" pod args)
(apply #'call-process "kubectl" nil buffer nil "logs" pod args))
(switch-to-buffer buffer)))
Simply define a mode map for kubernetes-mode
(defvar kubernetes-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "l") 'kubernetes-transient)
map))
(kubernetes)
- nicer method to get pods and other columns
- error handling
- no hard coded values
- customization
- could implement a lot of kubernetes functions, not just logs
- Gist of these notes
- Full extension for kubernetes
C-h f start-process
for doc on processes- Transient Manual
- Kubernets cheatsheet
Adrien, you gave an outstanding presentation tonight. I did not know about the Transient package. For that alone, many thanks!
Tom Davey