From adc27bc960518329c4d098705909978bd890c28b Mon Sep 17 00:00:00 2001 From: Diego Alvarez Date: Sat, 9 Sep 2023 12:39:39 +0000 Subject: [PATCH 1/2] Multi buffers support (take #57 and rebase) --- README.md | 6 +++ kubel.el | 159 +++++++++++++++++++++++++++++++++++------------------- 2 files changed, 109 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 7898941..e1d752e 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ We now support managing pretty much any resource! - exec into a pod using tramp - quick run shell-command - scale replicas +- multiple kubel buffers, each one with different context, namespace, and resource. ## Installation @@ -76,6 +77,11 @@ To programmatically open a session for a specific context/namespace/resource, ca (kubel-open "" "" "") ``` +Each kubel buffer will automatically be renamed using the following template: +``` +*kubel session: ||||* +``` + ## Shortcuts On the kubel screen, place your cursor on a resource diff --git a/kubel.el b/kubel.el index 79b6273..204793c 100644 --- a/kubel.el +++ b/kubel.el @@ -245,7 +245,7 @@ off - always assume we cannot list namespaces" (goto-char (point-max)) (insert (format "%s\n" str)))) -(defvar kubel--last-command nil) +(defvar-local kubel--last-command nil) (defun kubel--log-command (process-name cmd) "Log the kubectl command to the process buffer. @@ -266,23 +266,28 @@ CMD is the command string to run." (with-current-buffer standard-output (shell-command cmd t "*kubel stderr*")))) -(defvar kubel-namespace "default" +(defvar-local kubel-namespace "default" "Current namespace.") -(defvar kubel-resource "Pods" +(defvar-local kubel-resource "Pods" "Current resource.") -(defvar kubel-context +(defvar-local 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 "" +(defvar-local kubel-resource-filter "" "Substring filter for resource name.") -(defvar kubel-selector "" +(defvar-local kubel-selector "" "Label selector for resources.") +(defvar-local kubel--line-number nil + "Store the current line number to jump back after a refresh.") + +(defvar-local kubel-last-position nil) + (defvar kubel-namespace-history '() "List of previously used namespaces.") @@ -315,17 +320,17 @@ CMD is the command string to run." "RoleBindings" "Roles")) -(defvar kubel--kubernetes-version-cached nil) +(defvar-local kubel--kubernetes-version-cached nil) (defvar kubel--kubernetes-resources-list-cached nil) -(defvar kubel--can-get-namespace-cached nil) +(defvar-local kubel--can-get-namespace-cached nil) (defvar kubel--namespace-list-cached nil) -(defvar kubel--label-values-cached nil) +(defvar-local kubel--label-values-cached nil) -(defvar kubel--selected-items '()) +(defvar-local kubel--selected-items '()) (defun kubel--invalidate-context-caches () "Invalidate the context caches." @@ -477,12 +482,15 @@ If MAX is the end of the line, dynamically adjust." "Return the width of a specific COLNUM in ENTRYLIST." (seq-max (mapcar (lambda (x) (length (nth colnum x) )) entrylist))) +(defun kubel--buffer-name-from-parameters (context namespace resource) + "Return a preconfigured kubel buffer name." + (concat (format "*kubel session: |%s|%s|%s|*" context namespace resource))) + (defun kubel--buffer-name () "Return kubel buffer name." - (concat (format "*kubel (%s) [%s]: %s" kubel-namespace kubel-context kubel-resource) + (concat (kubel--buffer-name-from-parameters kubel-context kubel-namespace kubel-resource) (unless (equal kubel-selector "") - (format " (%s)" kubel-selector)) - "*")) + (format " (%s)" kubel-selector)))) (defun kubel--items-selected-p () "Return non-nil if there are items selected." @@ -531,7 +539,7 @@ 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)) + (let ((buffer-name (format "*kubel resource: |%s|%s|%s|*" kubel-context kubel-namespace (string-join args "_"))) (error-buffer (kubel--process-error-buffer process-name)) (cmd (append (list kubel-kubectl) (kubel--get-context-namespace) args))) (when (get-buffer buffer-name) @@ -656,7 +664,7 @@ TYPENAME is the resource type/name." (equal (capitalize kubel-resource) "Pods")) (defun kubel--is-deployment-view () - "Return non-nil if this is the pod view." + "Return non-nil if this is a deployment view." (-contains? '("Deployments" "deployments" "deployments.apps") kubel-resource)) (defun kubel--is-scalable () @@ -667,12 +675,13 @@ TYPENAME is the resource type/name." (-contains? '("StatefulSets" "statefulsets" "statefulsets.apps") kubel-resource))) ;; interactive +;;;###autoload (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) + (define-key map (kbd "C-c C-c") #'kubel-apply) map)) (defun kubel-apply () @@ -683,7 +692,6 @@ Use C-c C-c to kubectl apply the current yaml buffer." (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)))) @@ -701,15 +709,20 @@ Use C-c C-c to kubectl apply the current yaml buffer." DESCRIBE is the optional param to describe instead of get." (interactive "P") (let* ((resource (kubel--get-resource-under-cursor)) + (ctx kubel-context) + (ns kubel-namespace) + (res kubel-resource) (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)))) - + (kubel-yaml-editing-mode) + (setq kubel-context ctx) + (setq kubel-namespace ns) + (setq kubel-resource res) + (goto-char (point-min))))) (defun kubel--default-tail-arg (args) "Ugly function to make sure that there is at least the default tail. @@ -837,10 +850,9 @@ ARGS is the arguments list from transient." (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))) + (kubel-refresh last-default-directory))) (defun kubel-set-context () "Set the context." @@ -854,7 +866,7 @@ ARGS is the arguments list from transient." (when kubel--buffer (kill-buffer kubel--buffer));; kill buffer for previous context if possible (kubel--invalidate-context-caches) (setq kubel-namespace "default") - (kubel last-default-directory))) + (kubel-refresh last-default-directory))) (defun kubel--add-selector-to-history (selector) "Add SELECTOR to history if it isn't there already." @@ -883,8 +895,9 @@ ARGS is the arguments list from transient." (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)) + (kubel--add-selector-to-history kubel-selector) + ;; Update pod list according to the label selector + (kubel-refresh)) (defun kubel--fetch-api-resource-list () "Fetch the API resource list." @@ -907,9 +920,8 @@ the context caches, including the cached resource 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))) + (completing-read "Select resource: " resource-list)) + (kubel-refresh last-default-directory))) (defun kubel-set-output-format () "Set output format of kubectl." @@ -917,8 +929,9 @@ the context caches, including the cached resource list." (setq kubel-output (completing-read "Set output format: " - '("yaml" "json" "wide" "custom-columns="))) - (kubel)) + (completing-read + "Set output format: " + '("yaml" "json" "wide" "custom-columns="))))) (defun kubel-port-forward-pod (p) "Port forward a pod to your local machine. @@ -1077,7 +1090,7 @@ REPLICAS is the number of desired replicas." FILTER is the filter string." (interactive "MFilter: ") (setq kubel-resource-filter filter) - (kubel)) + (kubel-refresh)) (defun kubel--jump-to-highlight (init search reset) "Base function to jump to highlight. @@ -1138,7 +1151,7 @@ RESET is to be called if the search is nil after the first attempt." (progn (push item kubel--selected-items) (forward-line 1) - (kubel))))) + (kubel-refresh))))) (defun kubel-unmark-item () "Unmark the item under cursor." @@ -1147,7 +1160,7 @@ RESET is to be called if the search is nil after the first attempt." (when (-contains? kubel--selected-items item) (progn (setq kubel--selected-items (delete item kubel--selected-items)) - (kubel))))) + (kubel-refresh))))) (defun kubel-mark-all () "Mark all items." @@ -1158,13 +1171,13 @@ RESET is to be called if the search is nil after the first attempt." (while (not (eobp)) (push (kubel--get-resource-under-cursor) kubel--selected-items) (forward-line 1))) - (kubel)) + (kubel-refresh)) (defun kubel-unmark-all () "Unmark all items." (interactive) (setq kubel--selected-items '()) - (kubel)) + (kubel-refresh)) ;; popups @@ -1216,7 +1229,7 @@ RESET is to be called if the search is nil after the first attempt." ;; global ("RET" "Resource details" kubel-describe-popup) ("E" "Quick edit" kubel-quick-edit) - ("g" "Refresh" kubel) + ("g" "Refresh" kubel-refresh) ("k" "Delete" kubel-delete-popup) ("r" "Rollout" kubel-rollout-history)] ["" ;; based on current view @@ -1253,7 +1266,7 @@ RESET is to be called if the search is nil after the first attempt." (define-key map (kbd "K") 'kubel-set-kubectl-config-file) (define-key map (kbd "C") 'kubel-set-context) (define-key map (kbd "n") 'kubel-set-namespace) - (define-key map (kbd "g") 'kubel) + (define-key map (kbd "g") 'kubel-refresh) (define-key map (kbd "h") 'kubel-help-popup) (define-key map (kbd "?") 'kubel-help-popup) (define-key map (kbd "F") 'kubel-set-output-format) @@ -1295,16 +1308,64 @@ DIRECTORY is optional for TRAMP support." (setq kubel-resource (or resource "Pods")) (kubel directory)) + +(defun kubel--current-state () + "Show in the Echo Area the current context, namespace, and resource." + (message (concat + (format "[Context: %s] [Namespace: %s] [Resource: %s]" kubel-context kubel-namespace kubel-resource) + (unless (equal kubel-selector "") + (format " (%s)" kubel-selector))))) + ;;;###autoload -(defun kubel (&optional directory) - "Invoke the kubel buffer. +(defun kubel-refresh (&optional directory) + "Refresh the current kubel buffer, calling kubectl using the configured +context, namespace, and resource. DIRECTORY is optional for TRAMP support." (interactive) - (kubel--pop-to-buffer (kubel--buffer-name)) + (kubel--save-line) (when directory (setq default-directory directory)) - (kubel-mode) - (message (concat "Namespace: " kubel-namespace))) + (let ((name (kubel--buffer-name))) + (unless (get-buffer name) + (rename-buffer name)) + (message (format "Running kubectl for: %s..." name))) + (let ((entries (kubel--populate-list))) + (setq tabulated-list-format (car entries)) + (setq tabulated-list-entries (cadr entries))) ; TODO handle "No resource found" + (setq tabulated-list-sort-key kubel--list-sort-key) + (setq tabulated-list-sort-key nil) + (tabulated-list-init-header) + (tabulated-list-print) + (kubel--current-state) + (kubel--jump-back-to-line)) + +;;;###autoload +(defun kubel-open (context namespace resource &optional directory) + "Create a new kubel buffer using passed parameters CONTEXT NAMESPACE RESOURCE. +DIRECTORY is optional for TRAMP support." + (let ((tmpname "*kubel-tmp*") + (name (kubel--buffer-name-from-parameters context namespace resource))) + (if (get-buffer name) + (pop-to-buffer-same-window name) + (with-current-buffer (get-buffer-create tmpname) + (kubel-mode) + (setq kubel-context context) + (setq kubel-namespace namespace) + (setq kubel-resource resource) + (pop-to-buffer-same-window tmpname) + (kubel-refresh directory))))) + +;;;###autoload +(defun kubel (&optional directory) + "Invoke the kubel buffer. +DIRECTORY is optional for TRAMP support." + (interactive) + (let* ((name (kubel--buffer-name)) + (buf (generate-new-buffer name))) + (switch-to-buffer buf) + (with-current-buffer buf + (kubel-mode) + (kubel-refresh directory)))) (define-derived-mode kubel-mode tabulated-list-mode "Kubel" "Special mode for kubel buffers." @@ -1314,20 +1375,6 @@ DIRECTORY is optional for TRAMP support." (setq mode-name "Kubel") (setq major-mode 'kubel-mode) (use-local-map kubel-mode-map) - (let ((entries (kubel--populate-list))) - (setq tabulated-list-format (car entries)) - (setq tabulated-list-entries (cadr entries))) ; TODO handle "No resource found" - (setq tabulated-list-sort-key kubel--list-sort-key) - (setq tabulated-list-sort-key nil) - (tabulated-list-init-header) - (let ((line-num (line-number-at-pos (point))) - (current-id (tabulated-list-get-id))) - (tabulated-list-print t) - (unless (string-equal current-id (tabulated-list-get-id)) - ;; tabulated-list could not follow the current entry, then fallback on - ;; keeping the same line. - (goto-char (point-min)) - (forward-line (1- line-num)))) (hl-line-mode 1) (run-mode-hooks 'kubel-mode-hook)) From 668bf90d4f316defc8fcf40f04593e4074af0321 Mon Sep 17 00:00:00 2001 From: Giap Tran Date: Sun, 10 Sep 2023 23:48:53 +0000 Subject: [PATCH 2/2] Add kubel--read-buffer, improve kubel-refresh and kubel command --- kubel.el | 116 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 77 insertions(+), 39 deletions(-) diff --git a/kubel.el b/kubel.el index 204793c..f7f8d72 100644 --- a/kubel.el +++ b/kubel.el @@ -283,11 +283,6 @@ CMD is the command string to run." (defvar-local kubel-selector "" "Label selector for resources.") -(defvar-local kubel--line-number nil - "Store the current line number to jump back after a refresh.") - -(defvar-local kubel-last-position nil) - (defvar kubel-namespace-history '() "List of previously used namespaces.") @@ -484,7 +479,7 @@ If MAX is the end of the line, dynamically adjust." (defun kubel--buffer-name-from-parameters (context namespace resource) "Return a preconfigured kubel buffer name." - (concat (format "*kubel session: |%s|%s|%s|*" context namespace resource))) + (concat (format "*kubel:%s:%s:%s*" context namespace resource))) (defun kubel--buffer-name () "Return kubel buffer name." @@ -539,7 +534,7 @@ 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 "*kubel resource: |%s|%s|%s|*" kubel-context kubel-namespace (string-join args "_"))) + (let ((buffer-name (format "*kubel-resource:%s:%s:%s*" kubel-context kubel-namespace (string-join args "_"))) (error-buffer (kubel--process-error-buffer process-name)) (cmd (append (list kubel-kubectl) (kubel--get-context-namespace) args))) (when (get-buffer buffer-name) @@ -850,23 +845,26 @@ ARGS is the arguments list from transient." (kubel--buffer (get-buffer (kubel--buffer-name))) (last-default-directory (when kubel--buffer (with-current-buffer kubel--buffer default-directory)))) - (setq kubel-namespace namespace) - (kubel--add-namespace-to-history namespace) - (kubel-refresh last-default-directory))) + (with-current-buffer (clone-buffer) + (setq kubel-namespace namespace) + (kubel--add-namespace-to-history namespace) + (switch-to-buffer (current-buffer)) + (kubel-refresh 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)))) - (setq kubel-context - (completing-read - "Select context: " - (split-string (kubel--exec-to-string (format "%s config view -o jsonpath='{.contexts[*].name}'" kubel-kubectl)) " "))) - (when kubel--buffer (kill-buffer kubel--buffer));; kill buffer for previous context if possible - (kubel--invalidate-context-caches) - (setq kubel-namespace "default") - (kubel-refresh last-default-directory))) + (with-current-buffer (clone-buffer) + (setq kubel-context + (completing-read + "Select context: " + (split-string (kubel--exec-to-string (format "%s config view -o jsonpath='{.contexts[*].name}'" kubel-kubectl)) " "))) + (kubel--invalidate-context-caches) + (setq kubel-namespace "default") + (switch-to-buffer (current-buffer)) + (kubel-refresh last-default-directory)))) (defun kubel--add-selector-to-history (selector) "Add SELECTOR to history if it isn't there already." @@ -889,15 +887,17 @@ ARGS is the arguments list from transient." (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-refresh)) + (with-current-buffer (clone-buffer) + (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 + (switch-to-buffer (current-buffer)) + (kubel-refresh))) (defun kubel--fetch-api-resource-list () "Fetch the API resource list." @@ -919,9 +919,11 @@ the context caches, including the cached resource list." 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)) - (kubel-refresh last-default-directory))) + (with-current-buffer (clone-buffer) + (setq kubel-resource + (completing-read "Select resource: " resource-list)) + (switch-to-buffer (current-buffer)) + (kubel-refresh last-default-directory)))) (defun kubel-set-output-format () "Set output format of kubectl." @@ -1179,6 +1181,30 @@ RESET is to be called if the search is nil after the first attempt." (setq kubel--selected-items '()) (kubel-refresh)) +(defun kubel--read-buffer () + "Return the list of all buffers of kubel pattern." + (let* ((other-buffer (other-buffer (current-buffer))) + (other-name (buffer-name other-buffer)) + (buffers)) + (dolist (buf (buffer-list)) + (when (string-prefix-p "*kubel:" (buffer-name buf)) + (push buf buffers))) + (let ((predicate + (lambda (buffer) + ;; BUFFER is an entry (BUF-NAME . BUF-OBJ) of Vbuffer_alist. + (memq (cdr buffer) buffers)))) + (read-buffer + "Switch to buffer: " + (when (funcall predicate (cons other-name other-buffer)) other-name) + nil + predicate)))) + +(defun kubel-switch-to-buffer (buffer-or-name) + "Display buffer BUFFER-OR-NAME in the selected window. +When called interactively, prompts for a buffer belonging to kubel." + (interactive (list (kubel--read-buffer))) + (switch-to-buffer buffer-or-name)) + ;; popups (transient-define-prefix kubel-exec-popup () @@ -1230,6 +1256,7 @@ RESET is to be called if the search is nil after the first attempt." ("RET" "Resource details" kubel-describe-popup) ("E" "Quick edit" kubel-quick-edit) ("g" "Refresh" kubel-refresh) + ("b" "Buffers" kubel-switch-to-buffer) ("k" "Delete" kubel-delete-popup) ("r" "Rollout" kubel-rollout-history)] ["" ;; based on current view @@ -1279,6 +1306,7 @@ RESET is to be called if the search is nil after the first attempt." (define-key map (kbd "M-p") 'kubel-jump-to-previous-highlight) (define-key map (kbd "$") 'kubel-show-process-buffer) (define-key map (kbd "s") 'kubel-set-label-selector) + (define-key map (kbd "b") 'kubel-switch-to-buffer) ;; based on view (define-key map (kbd "p") 'kubel-port-forward-pod) (define-key map (kbd "S") 'kubel-scale-replicas) @@ -1323,11 +1351,13 @@ context, namespace, and resource. DIRECTORY is optional for TRAMP support." (interactive) - (kubel--save-line) (when directory (setq default-directory directory)) (let ((name (kubel--buffer-name))) - (unless (get-buffer name) - (rename-buffer name)) + ;; Remove old buffer if exist but not is current buffer + (if (get-buffer name) + (unless (equal (buffer-name (current-buffer)) name) + (kill-buffer (get-buffer name)))) + (rename-buffer name) (message (format "Running kubectl for: %s..." name))) (let ((entries (kubel--populate-list))) (setq tabulated-list-format (car entries)) @@ -1335,9 +1365,15 @@ DIRECTORY is optional for TRAMP support." (setq tabulated-list-sort-key kubel--list-sort-key) (setq tabulated-list-sort-key nil) (tabulated-list-init-header) - (tabulated-list-print) - (kubel--current-state) - (kubel--jump-back-to-line)) + (let ((line-num (line-number-at-pos (point))) + (current-id (tabulated-list-get-id))) + (tabulated-list-print t) + (unless (string-equal current-id (tabulated-list-get-id)) + ;; tabulated-list could not follow the current entry, then fallback on + ;; keeping the same line. + (goto-char (point-min)) + (forward-line (1- line-num)))) + (kubel--current-state)) ;;;###autoload (defun kubel-open (context namespace resource &optional directory) @@ -1361,10 +1397,12 @@ DIRECTORY is optional for TRAMP support." DIRECTORY is optional for TRAMP support." (interactive) (let* ((name (kubel--buffer-name)) - (buf (generate-new-buffer name))) - (switch-to-buffer buf) + (buf (or (get-buffer name) + (get-buffer-create name)))) (with-current-buffer buf - (kubel-mode) + (switch-to-buffer (current-buffer)) + (unless (eq major-mode 'kubel-mode) + (kubel-mode)) (kubel-refresh directory)))) (define-derived-mode kubel-mode tabulated-list-mode "Kubel"