Skip to content

Instantly share code, notes, and snippets.

@nfedyashev
Forked from abrochard/presentation.org
Created February 15, 2021 22:29
Show Gist options
  • Save nfedyashev/dd43cf31f5c3a94c75573c905530f41b to your computer and use it in GitHub Desktop.
Save nfedyashev/dd43cf31f5c3a94c75573c905530f41b to your computer and use it in GitHub Desktop.
Notes from the "Conquering Kubernetes with Emacs" presentation

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

kubectl get pods

First pass at massaging

Only keep pod name and discard first line

kubectl get pods --no-headers=true | awk '{print $1}'

Turn that into a lisp string

Using the shell-command-to-string function

(shell-command-to-string "kubectl get pods --no-headers=true | awk '{print $1}'")

Turn that into a lisp list

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")

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
    [("Col1" 50) ("Col2" 50)]
        

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
    (list '(nil ["row1" "value1"])
          '(nil ["row2" "value2"])
          '(nil ["row3" "value3"]))
        

Putting it all together

(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))

Dump our pod lists into tabulated-list-mode

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))

Make a major mode out of it

(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))

Testing it out

Can summon with M-x kubernetes or

(kubernetes)

Getting kubectl logs into a buffer

Requirements

  • async sub-process creation -> no hanging Emacs
  • redirect output to a buffer

Getting logs

kubectl logs redis-64896b74dc-zrw7w

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

(let ((buffer "*kubectl-logs*"))
  (call-process "kubectl" nil buffer nil "logs" "redis-64896b74dc-zrw7w")
  (switch-to-buffer buffer))

Proper way to call an async process

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))

Putting all that into a function

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

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:

(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)))

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

(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)

Transient with switches

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)

Transient with params

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)

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

(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)])

Our kubernetes-transient

  • 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)

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
(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)))

Connecting the transient to our mode

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))

Trying it out

(kubernetes)

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment