Skip to content

Instantly share code, notes, and snippets.

@abrochard
Last active October 20, 2024 16:02
Show Gist options
  • Save abrochard/dd610fc4673593b7cbce7a0176d897de to your computer and use it in GitHub Desktop.
Save abrochard/dd610fc4673593b7cbce7a0176d897de 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

@tomdavey
Copy link

tomdavey commented Jul 2, 2019

Adrien, you gave an outstanding presentation tonight. I did not know about the Transient package. For that alone, many thanks!

Tom Davey

@abrochard
Copy link
Author

Thank you Tom! Glad you liked it!

@vdemeester
Copy link

@abrochard Amazing presentation (watched on youtube 😅), this shows how easily it is to extends emacs and gave me quite some ideas 🤗 🎉

@abrochard
Copy link
Author

abrochard commented Jul 26, 2019 via email

@livtanong
Copy link

I liked the presentation! Not sure if you figured it out, but I thought I'd say it anyway: The column span isn't a percentage. It's the number of characters.

@olymk2
Copy link

olymk2 commented Jul 28, 2019

@abrochard awesome presentation learnt a lot, currently building my own transient based on this.
One question though when you add --message prefix it had a default argument of "hello meetup" I can't see how you set that default any suggestions ?

@abrochard
Copy link
Author

abrochard commented Jul 28, 2019 via email

@abrochard
Copy link
Author

abrochard commented Jul 28, 2019 via email

@olymk2
Copy link

olymk2 commented Jul 28, 2019

@abrochard makes sense, I have asked a question on reddit about setting defaults, i found a few possible functions, seems you can manually toggle values just not sure how to inject that change when the transient is launched

@olymk2
Copy link

olymk2 commented Jul 28, 2019

@abrochard makes sense, I have asked a question on reddit about setting defaults, i found a few possible functions, seems you can manually toggle values just not sure how to inject that change when the transient is launched

@tarsius
Copy link

tarsius commented Jul 29, 2019

I liked the presentation too. Thanks!

Two things are a bit strange/incorrect and one of them involved that "hello world".

Transient supports history at the prefix and the infix level. What you saw here was the infix-level history. You can also move through history of the prefix itself using M-p/M-n.

As I have just learned I failed to document how to set a default value. Basically pass :value ("-foo" "--bar=val") to define-tranient-command. Also see my comment at https://www.reddit.com/r/emacs/comments/ciyqib/how_to_set_transient_infix_params_default_values/.

And the other issue concerned :shortarg. This is used as the key binding if no other method to do so was used. If the short-argument is different from the key binding that you want to use, then you should not set a wrong :shortarg instead set :shortarg to whatever is correct and also set :key. That way you can make use of transient-highlight-mismatched-keys (which see).

I would recommend using :key if you define your infix arguments using define-infix-argument. That in turn I recommend you only do if you add the argument to multiple prefixes and/or if the infix is complex. In other cases I recommend you contain the infix definitions in the prefix definition. See https://magit.vc/manual/transient/Suffix-Specifications.html.

@abrochard
Copy link
Author

Thank you so much @tarsius, it means a lot coming from you!

And thank you for the clarification, this makes a lot of sense. I will go update kubel to reflect that better practice!

@gitonthescene
Copy link

gitonthescene commented Jun 9, 2020

I admittedly have not yet done enough research. ( I plan too. ) But as someone coming from the outside, what's not clear to me about transient is how to handle switches which can be used multiple times ( like the --mount switch on docker, for example ). All the examples I've seen seem to assume a switch is only used once and only has a single value[1]. From my first look at the code, I presume I'll need to make a new class which inherits from transient-argument, and keeps a store of the multiple values and renders those in transient-format-value using the switch and use this object in a non-exiting infix command. I plan on trying to make this work in the next couple of days.

Does this sound off-base? Does anyone have any examples of a use case like this? There are certainly plenty of command line tools which support options being used multiple times. ( the -f option for rsync for example )

[1] The code for transient-format-value on a transient option seems to anticipate possible values, but I haven't yet tracked down how those can turn up.

@hjudt
Copy link

hjudt commented Mar 17, 2021

Note that for being able to execute the kubectl command on a remote machine, e.g. when working via tramp, you can use the replacement functions start-file-process and process-file.

@SreenivasVRao
Copy link

Stumbled on this while looking for some examples on Transient - very informative. Thanks!

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