-
Notifications
You must be signed in to change notification settings - Fork 44
/
kubel.el
1209 lines (1037 loc) · 43.7 KB
/
kubel.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; kubel.el --- Control Kubernetes with limited permissions -*- lexical-binding: t; -*-
;; Copyright (C) 2018, Adrien Brochard
;; This file is NOT part of Emacs.
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 2 of the
;; License, or (at your option) any later version.
;; This program is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program; if not, write to the Free Software
;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
;; USA
;; Version: 1.0
;; Author: Adrien Brochard
;; Keywords: kubernetes k8s tools processes
;; URL: https://github.com/abrochard/kubel
;; License: GNU General Public License >= 3
;; Package-Requires: ((transient "0.1.0") (emacs "25.3") (dash "2.12.0") (s "1.2.0") (yaml-mode "0.0.14"))
;;; Commentary:
;; Emacs extension for controlling Kubernetes with limited permissions.
;;; Usage:
;; To list the pods in your current context and namespace, call
;;
;; M-x kubel
;;
;; To set the path to the kubectl config file, call:
;; M-x kubel-set-kubectl-config-file
;;
;; or
;;
;; (kubel-set-kubectl-config-file <path to desired config file>)
;; ex: (kubel-set-kubectl-config-file "~/.kube/another-config")
;;
;; To set said namespace and context, respectively call
;;
;; M-x kubel-set-namespace
;; M-x kubel-set-context
;;
;; Note that namespace will autocomplete but not context,
;; this is because I interact with kubernetes through a user who
;; does not have permissions to list namespaces.
;;
;; To switch to showing a different resource, use the `R` command or
;;
;; M-x kubel-set-resource
;;
;; This will let you select a resource and re-display the kubel buffer.
;;; Shortcuts:
;; On the kubel screen, place your cursor on a resource
;;
;; enter => get resource details
;; C-u enter => describe resource
;; h => help popup
;; ? => help popup
;; E => quick edit any resource
;; g => refresh
;; k => delete popup
;; r => see the rollout history for resource
;; p => port forward pod
;; l => log popup
;; e => exec popup
;; j => jab deployment to force rolling update
;; S => scale replicas
;; C => set context
;; n => set namespace
;; R => set resource
;; K => set kubectl config file
;; F => set output format
;; f => set a substring filter for resource name
;; M-n => jump to the next highlighted resource
;; M-p => jump to previous highlighted resource
;; m => mark item
;; u => unmark item
;; M => mark all items
;; U => unmark all items
;; c => copy popup
;; $ => show process buffer
;; s => show only resources with specified label value
;;; Customize:
;; By default, kubel log tails from the last 100 lines, you can change the `kubel-log-tail-n` variable to set another line number.
;;; Code:
(require 'transient)
(require 'dash)
(require 's)
(require 'yaml-mode)
(require 'tramp)
(require 'subr-x)
(require 'eshell)
(require 'dired)
(defgroup kubel nil "Customisation group for kubel."
:group 'extensions)
(defvar kubel--list-format
[("Name" 50 t)
("Ready" 10 t)
("Status" 20 t)
("Restarts" 10 t)
("Age" 15 t)]
"List format.")
(defconst kubel--list-sort-key
'("NAME" . nil)
"Sort table on this key.")
(defconst kubel--status-colors
'(("Running" . "green")
("Healthy" . "green")
("Active" . "green")
("Ready" . "green")
("True" . "green")
("Unknown" . "orange")
("Error" . "red")
("Evicted" . "red")
("MemoryPressure" . "red")
("PIDPressure" . "red")
("DiskPressure" . "red")
("RevisionMissing" . "red")
("RevisionFailed" . "red")
("NetworkUnavailable" . "red")
("Completed" . "yellow")
("CrashLoopBackOff" . "red")
("Terminating" . "blue"))
"Associative list of status to color.")
(defconst kubel--process-buffer "*kubel-process*"
"Kubel process buffer name.")
(defcustom kubel-output "yaml"
"Format for output: json|yaml|wide|custom-columns=..."
:type 'string
:group 'kubel)
(defcustom kubel-log-tail-n 100
"Default number of lines to tail."
:type 'integer
:group 'kubel)
(defcustom kubel-use-namespace-list 'auto
"Control behavior for namespace completion.
auto - default, use `kubectl auth can-i list namespace` to determine if we can list namespaces
on - always assume we can list namespaces
off - always assume we cannot list namespaces"
:type 'symbol
:group 'kubel
:options '('auto 'on 'off))
(defun kubel--append-to-process-buffer (str)
"Append string STR to the process buffer."
(with-current-buffer (get-buffer-create kubel--process-buffer)
(read-only-mode -1)
(goto-char (point-max))
(insert (format "%s\n" str))))
(defvar kubel--last-command nil)
(defun kubel--log-command (process-name cmd)
"Log the kubectl command to the process buffer.
PROCESS-NAME is the name of the process.
CMD is the kubectl command as a list."
(let ((str-cmd (if (equal 'string (type-of cmd)) cmd (mapconcat #'identity cmd " "))))
(setq kubel--last-command str-cmd)
(kubel--append-to-process-buffer
(format "[%s]\ncommand: %s" process-name str-cmd))))
(defun kubel--exec-to-string (cmd)
"Replace \"shell-command-to-string\" to log to process buffer.
CMD is the command string to run."
(kubel--log-command "kubectl-command" cmd)
(shell-command-to-string cmd))
(defvar kubel-namespace "default"
"Current namespace.")
(defvar kubel-resource "Pods"
"Current resource.")
(defvar kubel-context
(replace-regexp-in-string
"\n" "" (kubel--exec-to-string "kubectl config current-context"))
"Current context. Tries to smart default.")
(defvar kubel-resource-filter ""
"Substring filter for resource name.")
(defvar kubel-selector ""
"Label selector for resources.")
(defvar kubel--line-number nil
"Store the current line number to jump back after a refresh.")
(defvar kubel-namespace-history '()
"List of previously used namespaces.")
(defvar kubel-selector-history '()
"List of previously used selectors.")
;; fallback list of resources if the version of kubectl doesn't support api-resources command
(defvar kubel-kubernetes-resources-list
'("Pods"
"Services"
"Namespaces"
"Nodes"
"Configmaps"
"Secrets"
"Bindings"
"PersistentVolumeClaims"
"PersistentVolumes"
"ReplicationControllers"
"ResourceQuotas"
"ServiceAccounts"
"Deployments"
"DaemonSets"
"ReplicaSets"
"StatefulSets"
"Jobs"
"hpa"
"Images"
"Ingresses"
"ClusterRoles"
"RoleBindings"
"Roles"))
(defvar kubel--kubernetes-version-cached nil)
(defvar kubel--kubernetes-resources-list-cached nil)
(defvar kubel--can-get-namespace-cached nil)
(defvar kubel--namespace-list-cached nil)
(defvar kubel--label-values-cached nil)
(defvar kubel--selected-items '())
(defun kubel--invalidate-context-caches ()
"Invalidate the context caches."
(setq kubel--kubernetes-resources-list-cached nil)
(setq kubel--kubernetes-version-cached nil)
(setq kubel--can-get-namespace-cached nil)
(setq kubel--namespace-list-cached nil)
(setq kubel--label-values-cached nil))
(defun kubel-kubernetes-version ()
"Return a list with (major-version minor-version patch)."
(let ((version-string (if (null kubel--kubernetes-version-cached)
(setq kubel--kubernetes-version-cached
(kubel--exec-to-string "kubectl version"))
kubel--kubernetes-version-cached)))
(string-match "GitVersion:\"v\\([0-9]*\\)\.\\([0-9]*\\)\.\\([0-9]*\\)[^0-9].*\"" version-string)
(list
(string-to-number (match-string 1 version-string))
(string-to-number (match-string 2 version-string))
(string-to-number (match-string 3 version-string)))))
(defun kubel-kubernetes-compatible-p (version)
"Return TRUE if kubernetes version is greater than or equal to VERSION.
VERSION should be a list of (major-version minor-version patch)."
(let*
((kubernetes-version (kubel-kubernetes-version))
(kubernetes-major-version (nth 0 kubernetes-version))
(kubernetes-minor-version (nth 1 kubernetes-version))
(kubernetes-patch-version (nth 2 kubernetes-version)))
(and
(<= (nth 0 version) kubernetes-major-version)
(or (<= (nth 1 version) kubernetes-minor-version) (< (nth 0 version) kubernetes-major-version))
(or (<= (nth 2 version) kubernetes-patch-version) (< (nth 1 version) kubernetes-minor-version)))))
(defun kubel--populate-list ()
"Return a list with a tabulated list format and \"tabulated-list-entries\"."
(let* ((body (kubel--exec-to-string (concat (kubel--get-command-prefix) " get " kubel-resource)))
(entrylist (kubel--parse-body body)))
(when (string-prefix-p "No resources found" body)
(message "No resources found")) ;; TODO exception here
(list (kubel--get-list-format entrylist) (kubel--get-list-entries entrylist))))
(defun kubel--column-entry (entrylist)
"Return a function of colnum to retrieve an entry in a given column for ENTRYLIST."
(function
(lambda (colnum)
(list (kubel--column-header entrylist colnum) (+ 4 (kubel--column-width entrylist colnum)) t))))
(defun kubel--get-list-format (entrylist)
"Get the list format.
ENTRYLIST is the output of the parsed body."
(defun kubel--get-column-entry (colnum)
(let ((kubel--get-entry (kubel--column-entry entrylist)))
(funcall kubel--get-entry colnum)))
(cl-map 'vector #'kubel--get-column-entry (number-sequence 0 (- (kubel--ncols entrylist) 1))))
(defun kubel--update-selected-items (entries)
"Check that all selected items still exist.
ENTRIES are all resources."
(dolist (i (-difference kubel--selected-items (mapcar #'car entries)))
(setq kubel--selected-items (delete i kubel--selected-items))))
(defun kubel--get-list-entries (entrylist)
"Get the entries.
ENTRYLIST is the output of the parsed body."
(let ((entries (cdr entrylist)))
(kubel--update-selected-items entries)
(mapcar (lambda (x)
(list (car x)
(vconcat [] (mapcar #'kubel--propertize-status x))))
entries)))
(defun kubel--parse-body (body)
"Parse the body of kubectl get resource call into a list.
BODY is the raw output of kubectl get resource."
(let* ((lines (nbutlast (split-string body "\n")))
(header (car lines))
;; Cronjobs have a "LAST SCHEDULE" column, so need to split on 2+ whitespace chars.
(cols (split-string header (rx (>= 2 whitespace)) t))
(start-pos (mapcar (lambda (x) (string-match x header)) cols))
(end-pos (delete 0 (append start-pos '("end"))))
(position (-zip-with 'cons start-pos end-pos))
(parse-line (lambda (line)
(mapcar (lambda (pos)
(kubel--extract-value line (car pos) (cdr pos)))
position))))
(mapcar parse-line lines)))
(defun kubel--extract-value (line min max)
"Extract value from LINE between MIN and MAX.
If it's just white space, return -, else trim space.
If MAX is the end of the line, dynamically adjust."
(let* ((maxx (if (equal max "end") (length line) max))
(str (substring-no-properties line min maxx)))
(if (string-match "^ +$" str)
"-"
(string-trim str))))
(defun kubel--ncols (entrylist)
"Return the number of columns in ENTRYLIST."
(length (car entrylist)))
(defun kubel--nrows (entrylist)
"Return the nubmer of rows in ENTRYLIST."
(length entrylist))
(defun kubel--column-header (entrylist colnum)
"Return the header for a specific COLNUM in ENTRYLIST."
(nth colnum (car entrylist)))
(defun kubel--column-width (entrylist colnum)
"Return the width of a specific COLNUM in ENTRYLIST."
(seq-max (mapcar (lambda (x) (length (nth colnum x) )) entrylist)))
(defun kubel--buffer-name ()
"Return kubel buffer name."
(concat (format "*kubel (%s) [%s]: %s" kubel-namespace kubel-context kubel-resource)
(unless (equal kubel-selector "")
(format " (%s)" kubel-selector))
"*"))
(defun kubel--items-selected-p ()
"Return non-nil if there are items selected."
(>= (length kubel--selected-items) 1))
(defun kubel--propertize-status (status)
"Return the status in proper font color.
STATUS is the pod status string."
(let ((pair (cdr (assoc status kubel--status-colors)))
(match (or (equal kubel-resource-filter "") (string-match-p kubel-resource-filter status)))
(selected (and (kubel--items-selected-p) (-contains? kubel--selected-items status))))
(cond (pair (propertize status 'font-lock-face `(:foreground ,pair)))
(selected (propertize (concat "*" status) 'face 'dired-marked))
((not match) (propertize status 'font-lock-face '(:foreground "darkgrey")))
(t status))))
(defun kubel--pop-to-buffer (name)
"Utility function to pop to buffer or create it.
NAME is the buffer name."
(unless (get-buffer name)
(get-buffer-create name))
(pop-to-buffer-same-window name))
(defun kubel--process-error-buffer (process-name)
"Return the error buffer name for the PROCESS-NAME."
(format "*%s:err*" process-name))
(defun kubel--sentinel (process _)
"Sentinel function for PROCESS."
(let ((process-name (process-name process))
(exit-status (process-exit-status process)))
(kubel--append-to-process-buffer (format "[%s]\nexit-code: %s" process-name exit-status))
(unless (eq 0 exit-status)
(let ((err (with-current-buffer (kubel--process-error-buffer process-name)
(buffer-string))))
(kubel--append-to-process-buffer (format "error: %s" err))
(error (format "Kubel process %s error: %s" process-name err))))))
(defun kubel--exec (process-name args &optional readonly)
"Utility function to run commands in the proper context and namespace.
PROCESS-NAME is an identifier for the process. Default to \"kubel-command\".
ARGS is a ist of arguments.
READONLY If true buffer will be in readonly mode(view-mode)."
(when (equal process-name "")
(setq process-name "kubel-command"))
(let ((buffer-name (format "*%s*" process-name))
(error-buffer (kubel--process-error-buffer process-name))
(cmd (append (list "kubectl") (kubel--get-context-namespace) args)))
(when (get-buffer buffer-name)
(kill-buffer buffer-name))
(when (get-buffer error-buffer)
(kill-buffer error-buffer))
(kubel--log-command process-name cmd)
(make-process :name process-name
:buffer buffer-name
:sentinel #'kubel--sentinel
:file-handler t
:stderr error-buffer
:command cmd)
(pop-to-buffer buffer-name)
(if readonly
(with-current-buffer buffer-name
(view-mode)))))
(defun kubel--get-resource-under-cursor ()
"Utility function to get the name of the resource under the cursor.
Strip the `*` prefix if the resource is selected"
(replace-regexp-in-string
"^\*" "" (aref (tabulated-list-get-entry) 0)))
(defun kubel--get-context-namespace ()
"Utility function to return the proper context and namespace arguments."
(append
(unless (equal kubel-context "")
(list "--context" kubel-context))
(unless (equal kubel-namespace "default")
(list "-n" kubel-namespace))))
(defun kubel--get-selector ()
"Utility function to return current label selector."
(unless (equal kubel-selector "")
(list "--selector" kubel-selector)))
(defun kubel--get-command-prefix ()
"Utility function to prefix the kubectl command with proper context and namespace."
(mapconcat 'identity (append '("kubectl") (kubel--get-context-namespace) (kubel--get-selector)) " "))
(defun kubel--get-containers (pod-name &optional type)
"List the containers in a pod.
POD-NAME is the name of the pod.
TYPE is containers or initContainers."
(unless type (setq type "containers"))
(split-string
(kubel--exec-to-string
(format "%s get pod %s -o jsonpath='{.spec.%s[*].name}'" (kubel--get-command-prefix) pod-name type)) " "))
(defun kubel--get-pod-labels ()
"List labels of pods in a current namespace."
(let* ((raw-labels
(split-string
(replace-regexp-in-string
(regexp-quote ":") "="
(replace-regexp-in-string
"map\\[\\(.+?\\)\\]" "\\1"
(kubel--exec-to-string
(format "%s get pod -o jsonpath='{.items[*].metadata.labels}'" (kubel--get-command-prefix)))))))
(splitted (mapcan (lambda (s) (split-string s ","))
raw-labels))
(cleaned (mapcar (lambda (s) (replace-regexp-in-string "[{|\"|}]" "" s)) splitted))
(unique (-distinct cleaned)))
unique))
(defun kubel--select-resource (name)
"Prompt user to select an instance out of a list of resources.
NAME is the string name of the resource."
(let ((cmd (format "%s get %s -o=jsonpath='{.items[*].metadata.name}'"
(kubel--get-command-prefix) name)))
(completing-read (concat (s-upper-camel-case name) ": ")
(split-string (kubel--exec-to-string cmd) " "))))
(defun kubel--describe-resource (name &optional describe)
"Describe a specific resource.
NAME is the string name of the resource to decribe.
DESCRIBE is boolean to describe instead of get resource details"
(let* ((resource (kubel--select-resource name))
(process-name (format "kubel - %s - %s" name resource)))
(if describe
(kubel--exec process-name (list "describe" name resource))
(kubel--exec process-name (list "get" name "-o" kubel-output resource)))
(when (string-equal kubel-output "yaml")
(yaml-mode)
(kubel-yaml-editing-mode))
(goto-char (point-min))))
(defun kubel--show-rollout-revision (type name)
"Show a specific revision of a certain resource.
TYPE is the resource type.
NAME is the resource name."
(let* ((typename (format "%s/%s" type name))
(revision (car (split-string (kubel--select-rollout typename))))
(process-name (format "kubel - rollout - %s - %s" typename revision)))
(kubel--exec process-name
(list "rollout" "history" typename (format "--revision=%s" revision)))
(goto-char (point-min))))
(defun kubel--list-rollout (typename)
"Return a list of revisions with format '%number %cause'.
TYPENAME is the resource type/name."
(let ((cmd (format "%s rollout history %s" (kubel--get-command-prefix) typename)))
(nthcdr 2 (split-string (kubel--exec-to-string cmd) "\n"))))
(defun kubel--select-rollout (typename)
"Select a rollout version.
TYPENAME is the resource type/name."
(let ((prompt (format "Select a rollout of %s: " typename))
(rollouts (kubel--list-rollout typename)))
(completing-read prompt rollouts)))
(defun kubel--is-pod-view ()
"Return non-nil if this is the pod view."
(equal (capitalize kubel-resource) "Pods"))
(defun kubel--is-deployment-view ()
"Return non-nil if this is the pod view."
(-contains? '("Deployments" "deployments" "deployments.apps") kubel-resource))
(defun kubel--is-scalable ()
"Return non-nil if the resource can be scaled."
(or
(kubel--is-deployment-view)
(-contains? '("ReplicaSets" "replicasets" "replicasets.apps") kubel-resource)
(-contains? '("StatefulSets" "statefulsets" "statefulsets.apps") kubel-resource)))
(defun kubel--save-line ()
"Save the current line number if the view is unchanged."
(if (equal (buffer-name (current-buffer))
(kubel--buffer-name))
(setq kubel--line-number (+ 1 (count-lines 1 (point))))
(setq kubel--line-number nil)))
(defun kubel--jump-back-to-line ()
"Jump back to the last cached line number."
(when kubel--line-number
(goto-line kubel--line-number)))
;; interactive
(define-minor-mode kubel-yaml-editing-mode
"Kubel Yaml editing mode.
Use C-c C-c to kubectl apply the current yaml buffer."
:init-value nil
:keymap (let ((map (make-sparse-keymap)))
(define-key map (kbd "C-c C-c") 'kubel-apply)
map))
(defun kubel-apply ()
"Save the current buffer to a temp file and try to kubectl apply it."
(interactive)
(setq dir-prefix (or
(when (tramp-tramp-file-p default-directory)
(with-parsed-tramp-file-name default-directory nil
(format "/%s%s:%s@%s:" (or hop "") method user host)))
""))
(let* ((filename-without-tramp-prefix (format "/tmp/kubel/%s-%s.yaml"
(replace-regexp-in-string "\*\\| " "" (buffer-name))
(floor (float-time))))
(filename (format "%s%s" dir-prefix filename-without-tramp-prefix)))
(when (y-or-n-p "Apply the changes? ")
(unless (file-exists-p (format "%s/tmp/kubel" dir-prefix))
(make-directory (format "%s/tmp/kubel" dir-prefix) t))
(write-region (point-min) (point-max) filename)
(kubel--exec (format "kubectl - apply - %s" filename) (list "apply" "-f" filename-without-tramp-prefix))
(message "Applied %s" filename))))
(defun kubel-get-resource-details (&optional describe)
"Get the details of the resource under the cursor.
DESCRIBE is the optional param to describe instead of get."
(interactive "P")
(let* ((resource (kubel--get-resource-under-cursor))
(process-name (format "kubel - %s - %s" kubel-resource resource)))
(if describe
(kubel--exec process-name (list "describe" kubel-resource (kubel--get-resource-under-cursor)))
(kubel--exec process-name (list "get" kubel-resource (kubel--get-resource-under-cursor) "-o" kubel-output)))
(when (or (string-equal kubel-output "yaml") (transient-args 'kubel-describe-popup))
(yaml-mode)
(kubel-yaml-editing-mode))
(goto-char (point-min))))
(defun kubel--default-tail-arg (args)
"Ugly function to make sure that there is at least the default tail.
ARGS is the arg list from transient."
(if (car (remove nil (mapcar (lambda (x)
(string-prefix-p "--tail=" x)) args)))
args
(append args (list (concat "--tail=" (format "%s" kubel-log-tail-n))))))
(defun kubel-get-pod-logs (&optional args type)
"Get the last N logs of the pod under the cursor.
ARGS is the arguments list from transient.
TYPE is containers or initContainers."
(interactive
(list (transient-args 'kubel-log-popup)))
(dolist (pod (if (kubel--is-pod-view)
(if (kubel--items-selected-p)
kubel--selected-items
(list (kubel--get-resource-under-cursor)))
(list (kubel--select-resource "Pods"))))
(let* ((type (or type "containers"))
(containers (kubel--get-containers pod type))
(container (if (equal (length containers) 1)
(car containers)
(completing-read "Select container: " containers)))
(process-name (format "kubel - logs - %s - %s" pod container)))
(kubel--exec process-name
(append '("logs") (kubel--default-tail-arg args) (list pod container)) t))))
(defun kubel-get-pod-logs--initContainer (&optional args)
"Get the last N logs of the pod under the cursor.
ARGS is the arguments list from transient."
(interactive
(list (transient-args 'kubel-log-popup)))
(kubel-get-pod-logs args "initContainers"))
(defun kubel-get-logs-by-labels (&optional args)
"Get the last N logs of the pods by labels.
ARGS is the arguments list from transient."
(interactive
(list (transient-args 'kubel-log-popup)))
(let* ((labels (kubel--get-pod-labels))
(label (completing-read "Select container: " labels))
(process-name (format "kubel - logs - %s" label)))
(kubel--exec process-name
(append '("logs") (kubel--default-tail-arg args) '("-l") (list label)) t)))
(defun kubel-copy-resource-name ()
"Copy the name of the pod under the cursor."
(interactive)
(kill-new (kubel--get-resource-under-cursor))
(message "Resource name copied to kill-ring"))
(defun kubel-copy-log-command ()
"Copy the streaming log command of the pod under the cursor."
(interactive)
(kill-new
(format "%s logs -f --tail=%s %s"
(kubel--get-command-prefix)
kubel-log-tail-n
(if (kubel--is-pod-view)
(kubel--get-resource-under-cursor)
(kubel--select-resource "Pods"))))
(message "Log command copied to kill-ring"))
(defun kubel-copy-command-prefix ()
"Copy the kubectl command prefix."
(interactive)
(kill-new (kubel--get-command-prefix))
(message "Command prefix copied to kill-ring"))
(defun kubel-copy-last-command ()
"Copy the last kubectl command ran."
(interactive)
(kill-new kubel--last-command)
(message (concat "Last command copied: " kubel--last-command)))
(defun kubel-set-kubectl-config-file (configfile)
"Set the path to the kubectl CONFIGFILE."
(interactive "f")
(let ((configfile (or configfile "~/.kube/config")))
(if (file-exists-p (expand-file-name configfile))
(setenv "KUBECONFIG" (expand-file-name configfile))
(error "Kubectl config file '%s' does not exist!" configfile))))
(defun kubel--can-get-namespace ()
"Determine if permissions allow for `kubectl get namespace` in current context."
(cond ((eq kubel-use-namespace-list 'on) t)
((eq kubel-use-namespace-list 'auto)
(progn
(unless kubel--can-get-namespace-cached
(setq kubel--can-get-namespace-cached
(equal "yes\n"
(kubel--exec-to-string
(format "kubectl --context %s auth can-i list namespaces" kubel-context))))))
kubel--can-get-namespace-cached)))
(defun kubel--get-namespace ()
"Get namespaces for current context, try to recover from cache first."
(unless kubel--namespace-list-cached
(setq kubel--namespace-list-cached
(split-string (kubel--exec-to-string
(format "kubectl --context %s get namespace -o jsonpath='{.items[*].metadata.name}'" kubel-context)) " ")))
kubel--namespace-list-cached)
(defun kubel--list-namespace ()
"List namespace, either from history, or dynamically if possible."
(if (kubel--can-get-namespace)
(kubel--get-namespace)
kubel-namespace-history))
(defun kubel--add-namespace-to-history (namespace)
"Add NAMESPACE to history if it isn't there already."
(unless (member namespace kubel-namespace-history)
(push namespace kubel-namespace-history)))
(defun kubel-set-namespace ()
"Set the namespace."
(interactive)
(let* ((namespace (completing-read "Namespace: " (kubel--list-namespace)
nil nil nil nil "default"))
(kubel--buffer (get-buffer (kubel--buffer-name)))
(last-default-directory (when kubel--buffer
(with-current-buffer kubel--buffer default-directory))))
(when kubel--buffer (kill-buffer kubel--buffer))
(setq kubel-namespace namespace)
(kubel--add-namespace-to-history namespace)
(kubel last-default-directory)))
(defun kubel-set-context ()
"Set the context."
(interactive)
(let* ((kubel--buffer (get-buffer (kubel--buffer-name)))
(last-default-directory (when kubel--buffer (with-current-buffer kubel--buffer default-directory))))
(when kubel--buffer (kill-buffer kubel--buffer));; kill buffer for previous context if possible
(setq kubel-context
(completing-read
"Select context: "
(split-string (kubel--exec-to-string "kubectl config view -o jsonpath='{.contexts[*].name}'") " ")))
(kubel--invalidate-context-caches)
(setq kubel-namespace "default")
(kubel last-default-directory)))
(defun kubel--add-selector-to-history (selector)
"Add SELECTOR to history if it isn't there already."
(unless (member selector kubel-selector-history)
(push selector kubel-selector-history)))
(defun kubel--get-all-selectors ()
"Get all selectors."
(unless kubel--label-values-cached
(let ((labels (kubel--get-pod-labels)))
(setq kubel--label-values-cached labels)))
kubel--label-values-cached)
(defun kubel--list-selectors ()
"List selector expressions from history."
(delete-dups
(append '("none") (kubel--get-all-selectors)
kubel-selector-history)))
(defun kubel-set-label-selector ()
"Set the selector."
(interactive)
(let ((selector (completing-read
"Selector: "
(kubel--list-selectors))))
(when (equal selector "none")
(setq selector ""))
(setq kubel-selector selector))
(kubel--add-selector-to-history kubel-selector)
; Update pod list according to the label selector
(kubel))
(defun kubel--fetch-api-resource-list ()
"Fetch the API resource list."
(split-string (kubel--exec-to-string
(format "kubectl --context %s api-resources -o name --no-headers=true" kubel-context)) "\n"))
(defun kubel-set-resource (&optional refresh)
"Set the resource.
If called with a prefix argument REFRESH, refreshes
the context caches, including the cached resource list."
(interactive "P")
(when refresh (kubel--invalidate-context-caches))
(let* ((current-buffer-name (kubel--buffer-name))
(resource-list (if (kubel-kubernetes-compatible-p '(1 13 3))
(if (null kubel--kubernetes-resources-list-cached)
(setq kubel--kubernetes-resources-list-cached
(kubel--fetch-api-resource-list))
kubel--kubernetes-resources-list-cached)
kubel-kubernetes-resources-list))
(kubel--buffer (get-buffer current-buffer-name))
(last-default-directory (when kubel--buffer (with-current-buffer kubel--buffer default-directory))))
(setq kubel-resource
(completing-read "Select resource: " resource-list))
(when kubel--buffer (kill-buffer kubel--buffer)) ;; kill buffer for previous context if possible
(kubel last-default-directory)))
(defun kubel-set-output-format ()
"Set output format of kubectl."
(interactive)
(setq kubel-output
(completing-read
"Set output format: "
'("yaml" "json" "wide" "custom-columns=")))
(kubel))
(defun kubel-port-forward-pod (p)
"Port forward a pod to your local machine.
P can be a single number or a localhost:container port pair."
(interactive "sPort: ")
(let* ((port (if (string-match-p ":" p) p (format "%s:%s" p p)))
(pod (if (kubel--is-pod-view)
(kubel--get-resource-under-cursor)
(kubel--select-resource "Pods")))
(process-name (format "kubel - port-forward - %s:%s" pod port)))
(kubel--exec process-name (list "port-forward" pod port))))
(defun kubel-setup-tramp ()
"Setup a kubectl TRAMP."
(setq tramp-methods (delete (assoc "kubectl" tramp-methods) tramp-methods)) ;; cleanup previous tramp method
;; TODO error message if resource is not pod
(add-to-list 'tramp-methods
`("kubectl"
(tramp-login-program "kubectl")
(tramp-login-args (,(kubel--get-context-namespace) ("exec" "-it") ("-c" "%u") ("%h") ("sh")))
(tramp-remote-shell "sh")
(tramp-remote-shell-args ("-i" "-c"))))) ;; add the current context/namespace to tramp methods
(defun kubel-exec-pod ()
"Exec into the pod under the cursor -> `find-file."
(interactive)
(kubel-setup-tramp)
(let* ((dir-prefix (or
(when (tramp-tramp-file-p default-directory)
(with-parsed-tramp-file-name default-directory nil
(format "%s%s:%s@%s|" (or hop "") method user host)))""))
(pod (if (kubel--is-pod-view)
(kubel--get-resource-under-cursor)
(kubel--select-resource "Pods")))
(containers (kubel--get-containers pod))
(container (if (equal (length containers) 1)
(car containers)
(completing-read "Select container: " containers))))
(find-file (format "/%skubectl:%s@%s:/" dir-prefix container pod))))
(defun kubel-exec-shell-pod ()
"Exec into the pod under the cursor -> shell."
(interactive)
(kubel-setup-tramp)
(let* ((dir-prefix (or
(when (tramp-tramp-file-p default-directory)
(with-parsed-tramp-file-name default-directory nil
(format "%s%s:%s@%s|" (or hop "") method user host))) ""))
(pod (if (kubel--is-pod-view)
(kubel--get-resource-under-cursor)
(kubel--select-resource "Pods")))
(containers (kubel--get-containers pod))
(container (if (equal (length containers) 1)
(car containers)
(completing-read "Select container: " containers)))
(default-directory (format "/%skubectl:%s@%s:/" dir-prefix container pod)))
(shell (format "*kubel - shell - %s@%s*" container pod))))
(defun kubel-exec-eshell-pod ()
"Exec into the pod under the cursor -> eshell."
(interactive)
(kubel-setup-tramp)
(let* ((dir-prefix (or
(when (tramp-tramp-file-p default-directory)
(with-parsed-tramp-file-name default-directory nil
(format "%s%s:%s@%s|" (or hop "") method user host))) ""))
(pod (if (kubel--is-pod-view)
(kubel--get-resource-under-cursor)
(kubel--select-resource "Pods")))
(containers (kubel--get-containers pod))
(container (if (equal (length containers) 1)
(car containers)
(completing-read "Select container: " containers)))
(default-directory (format "/%skubectl:%s@%s:/" dir-prefix container pod))
(eshell-buffer-name (format "*kubel - eshell - %s@%s*" container pod)))
(eshell)))
(defun kubel-exec-pod-by-shell-command ()
"Prompt shell with kubectl exec command at pod under cursor."
(interactive)
(kubel-setup-tramp)
(let* ((pod (if (kubel--is-pod-view)
(kubel--get-resource-under-cursor)
(kubel--select-resource "Pods")))
(containers (kubel--get-containers pod))
(container (if (equal (length containers) 1)
(car containers)
(completing-read "Select container: " containers)))
(command (read-string "Shell command: " (format "%s exec %s -c %s -- " (kubel--get-command-prefix) pod container))))
(shell-command command)))
(defun kubel-delete-resource ()
"Kubectl delete resource under cursor."
(interactive)
(dolist (pod (if (kubel--items-selected-p)
kubel--selected-items
(list (kubel--get-resource-under-cursor))))
(let* ((process-name (format "kubel - delete %s - %s" kubel-resource pod))
(args (list "delete" kubel-resource pod)))
(when (transient-args 'kubel-delete-popup)
(setq args (append args (list "--force" "--grace-period=0"))))
(kubel--exec process-name args))))
(defun kubel-jab-deployment ()
"Make a trivial patch to force a new deployment.
See https://github.com/kubernetes/kubernetes/issues/27081"
(interactive)
(dolist (deployment (if (kubel--is-deployment-view)
(if (kubel--items-selected-p)
kubel--selected-items
(list (kubel--get-resource-under-cursor)))
(list (kubel--select-resource "Deployments"))))
(let ((process-name (format "kubel - bouncing - %s" deployment)))
(kubel--exec process-name (list "patch" "deployment" deployment "-p"
(format "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"%s\"}}}}}"
(round (time-to-seconds))))))))
(defun kubel-scale-replicas (replicas)
"Scale resource replicas.
REPLICAS is the number of desired replicas."
(interactive (list (read-number "Replicas: ")))
(if (kubel--is-scalable)
(let ((resource (kubel--get-resource-under-cursor)))
(kubel--exec (list "scale" kubel-resource resource "--replicas" (number-to-string replicas))))
(message
"[%s] cannot be scaled.\nOnly these resources can be scaled: [deployment, replica set, replication controller, and stateful set]."
kubel-resource)))
(defun kubel-set-filter (filter)
"Set the pod filter.
FILTER is the filter string."
(interactive "MFilter: ")
(setq kubel-resource-filter filter)
(kubel))
(defun kubel--jump-to-highlight (init search reset)
"Base function to jump to highlight.
INIT is to be called before searching.
SEARCH is to apply the search and can be repeated safely.
RESET is to be called if the search is nil after the first attempt."
(unless (equal kubel-resource-filter "")
(funcall init)
(unless (funcall search)
(funcall reset)
(funcall search))
(beginning-of-line)))
(defun kubel-jump-to-next-highlight ()
"Jump to the next hightlighted resrouce."
(interactive)
(kubel--jump-to-highlight
#'end-of-line
(lambda () (re-search-forward kubel-resource-filter (point-max) t))
#'beginning-of-buffer))
(defun kubel-jump-to-previous-highlight ()
"Jump to the previou highlighted resrouce."
(interactive)
(kubel--jump-to-highlight
#'beginning-of-line
(lambda () (re-search-backward kubel-resource-filter (point-min) t))
#'end-of-buffer))
(defun kubel-rollout-history ()