#+TITLE: Conquering Kubernetes with Emacs * Specific Annoying workflow Listing pods with =kubectl get pods=, then select a pod name and copy paste it into =kubectl logs [pod name]= * Why? - 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! * Key takeaways - making a major mode - interacting with sub-processes - providing a modern UX * Listing Pods ** Requirements - get pods from shell command with naive processing - dump that list in an appropriate major mode - derive our own major mode ** Get pods command #+begin_src shell :results value pp kubectl get pods #+end_src #+RESULTS: : NAME READY STATUS RESTARTS AGE : frontend-6f567b7966-6pgzs 1/1 Running 0 3d : hello-node-7f5b6bd6b8-48kk4 1/1 Running 0 3d : redis-64896b74dc-zrw7w 1/1 Running 0 3d ** First pass at massaging Only keep pod name and discard first line #+begin_src shell :results value pp kubectl get pods --no-headers=true | awk '{print $1}' #+end_src #+RESULTS: : frontend-6f567b7966-6pgzs : hello-node-7f5b6bd6b8-48kk4 : redis-64896b74dc-zrw7w ** Turn that into a lisp string Using the =shell-command-to-string= function #+begin_src emacs-lisp :results value code (shell-command-to-string "kubectl get pods --no-headers=true | awk '{print $1}'") #+end_src #+RESULTS: #+begin_src emacs-lisp "frontend-6f567b7966-6pgzs\nhello-node-7f5b6bd6b8-48kk4\nredis-64896b74dc-zrw7w\n" #+end_src ** Turn that into a lisp list Just split at every new line with =split-string= function #+begin_src emacs-lisp :results value code (split-string (shell-command-to-string "kubectl get pods --no-headers=true | awk '{print $1}'") "\n") #+end_src #+RESULTS: #+begin_src emacs-lisp ("frontend-6f567b7966-6pgzs" "hello-node-7f5b6bd6b8-48kk4" "redis-64896b74dc-zrw7w" "") #+end_src ** Tabulated list mode There's already a great major to display columns of data: =tabulated-list-mode=. *** Defining the columns The column format as a vector of =(name width)= elements where: - =name= is the column name - =width= is the column width #+begin_src emacs-lisp [("Col1" 50) ("Col2" 50)] #+end_src *** Defining the rows 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 #+begin_src emacs-lisp (list '(nil ["row1" "value1"]) '(nil ["row2" "value2"]) '(nil ["row3" "value3"])) #+end_src *** Putting it all together #+begin_src emacs-lisp (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)) #+end_src ** Dump our pod lists into tabulated-list-mode Set up only one column for pod name. #+begin_src emacs-lisp :results ouytput none (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)) #+end_src ** Make a major mode out of it #+begin_src emacs-lisp :results output silent (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)) #+end_src ** Testing it out Can summon with =M-x kubernetes= or #+begin_src emacs-lisp (kubernetes) #+end_src * Getting =kubectl logs= into a buffer ** Requirements - async sub-process creation -> no hanging Emacs - redirect output to a buffer ** Getting logs #+begin_src shell :results output pp kubectl logs redis-64896b74dc-zrw7w #+end_src #+RESULTS: #+begin_example _._ _.-``__ ''-._ _.-`` `. `_. ''-._ Redis 2.8.19 (00000000/0) 64 bit .-`` .-```. ```\/ _.,_ ''-._ ( ' , .-` | `, ) Running in stand alone mode |`-._`-...-` __...-.``-._|'` _.-'| Port: 6379 | `-._ `._ / _.-' | PID: 1 `-._ `-._ `-./ _.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | http://redis.io `-._ `-._`-.__.-'_.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | `-._ `-._`-.__.-'_.-' _.-' `-._ `-.__.-' _.-' `-._ _.-' `-.__.-' [1] 01 Jul 16:36:57.440 # Server started, Redis version 2.8.19 [1] 01 Jul 16:36:57.443 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled. [1] 01 Jul 16:36:57.443 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. [1] 01 Jul 16:36:57.444 * The server is now ready to accept connections on port 6379 #+end_example ** Why won't the naive way work Not async and meh performance ** Proper way to call a process Use the =call-process= function and direct it to a buffer #+begin_src emacs-lisp (let ((buffer "*kubectl-logs*")) (call-process "kubectl" nil buffer nil "logs" "redis-64896b74dc-zrw7w") (switch-to-buffer buffer)) #+end_src ** Proper way to call an async process Use the =start-process= function instead which will create a process for you #+begin_src emacs-lisp (let ((process "*kubectl*") (buffer "*kubectl-logs*")) (start-process process buffer "kubectl" "logs" "-f""redis-64896b74dc-zrw7w") (switch-to-buffer buffer)) #+end_src ** Putting all that into a function Let's use the optional arg #+begin_src emacs-lisp (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))) #+end_src Try it with =M-x kubernetes-get-logs= or =C-u M-x kubernetes-get-logs= ** How to connect that function to our major mode 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: #+begin_src emacs-lisp (aref (tabulated-list-get-entry) 0) #+end_src Putting everything together #+begin_src emacs-lisp (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))) #+end_src ** Testing it out Call kubernetes mode with =M-x kubernetes= and then look at the logs of pod under cursor with =M-x kubernetes-get-logs= * Modern UX ** Requirements - a meaningful UI for users to interact with our major modes - transient (from the magit project) is perfect for wrapping CLI tools ** A simple transient #+begin_src emacs-lisp :results output silent (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) #+end_src ** Transient with switches We can easily define command line switches in our transient interface. #+begin_src emacs-lisp :results output silent (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) #+end_src ** Transient with params A bit more complex than simple switches, params let users enter a value. #+begin_src emacs-lisp :results output silent (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) #+end_src *** EDIT 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 #+begin_src emacs-lisp (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)]) #+end_src ** Our =kubernetes-transient= - can just get logs - can follow logs with =-f= - can specify tail length =--tail=100= - can combine these options #+begin_src emacs-lisp :results output silent (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) #+end_src ** Updating our =kubernetes-get-logs= - read args from transient - check if =-f= is in args to do async or not - pass the args into the process functions #+begin_src emacs-lisp :results output silent (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))) #+end_src ** Connecting the transient to our mode Simply define a mode map for =kubernetes-mode= #+begin_src emacs-lisp :results output silent (defvar kubernetes-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "l") 'kubernetes-transient) map)) #+end_src ** Trying it out #+begin_src emacs-lisp :results output silent (kubernetes) #+end_src * Conclusion ** What could be improved - 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 ** Resources - [[https://gist.github.com/abrochard/dd610fc4673593b7cbce7a0176d897de][Gist of these notes]] - [[https://github.com/abrochard/kubel][Full extension for kubernetes]] - =C-h f start-process= for doc on processes - [[https://magit.vc/manual/transient.html][Transient Manual]] - [[https://kubernetes.io/docs/reference/kubectl/cheatsheet/][Kubernets cheatsheet]]