From 7a66802c36cb0b56beaa63ed2b69fe2f828499e9 Mon Sep 17 00:00:00 2001 From: Damian Barabonkov Date: Fri, 23 Jan 2026 08:42:25 -0800 Subject: [PATCH 01/14] feat: Enhance kubectl command execution with error logging --- kubel.el | 106 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 62 insertions(+), 44 deletions(-) diff --git a/kubel.el b/kubel.el index 5579606..a8e5325 100644 --- a/kubel.el +++ b/kubel.el @@ -293,14 +293,30 @@ CMD is the kubectl command as a list." (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." +(defun kubel--handle-stderr (stderr-content) + "Handle stderr output by logging to process buffer and displaying to user. +STDERR-CONTENT is the stderr string from a kubectl command." + (when (and stderr-content (not (string-empty-p stderr-content))) + (let ((trimmed (string-trim stderr-content))) + (kubel--append-to-process-buffer (format "stderr: %s" trimmed)) + (message "Kubel error: %s [Type `$' for details]" trimmed)))) + +(defun kubel--exec-sync (cmd) + "Run CMD synchronously and return output as string. +Logs to process buffer and displays any stderr via `kubel--handle-stderr'." (kubel--log-command "kubectl-command" cmd) - (with-output-to-string - (with-current-buffer standard-output - (shell-command cmd t "*kubel stderr*")))) + (let ((stderr-buffer (generate-new-buffer " *kubel-stderr-temp*")) + (result nil)) + (unwind-protect + (progn + (setq result + (with-output-to-string + (with-current-buffer standard-output + (shell-command cmd t stderr-buffer)))) + (kubel--handle-stderr (with-current-buffer stderr-buffer + (buffer-string)))) + (kill-buffer stderr-buffer)) + result)) (defvar-local kubel-namespace kubel-default-namespace "Current namespace.") @@ -310,7 +326,7 @@ CMD is the command string to run." (defvar-local kubel-context (replace-regexp-in-string - "\n" "" (kubel--exec-to-string "kubectl config current-context")) + "\n" "" (kubel--exec-sync "kubectl config current-context")) "Current context. Tries to smart default.") (defvar-local kubel-resource-filter "" @@ -351,7 +367,7 @@ CMD is the command string to run." (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 (if kubel-list-wide " -o wide" "")))) + (let* ((body (kubel--exec-sync (concat (kubel--get-command-prefix) " get " kubel-resource (if kubel-list-wide " -o wide" "")))) (entrylist (kubel--parse-body body))) (when (string-prefix-p "No resources found" body) (message "No resources found")) ;; TODO exception here @@ -502,28 +518,28 @@ NAME is the buffer name." (format "*%s:err*" process-name)) (defun kubel--sentinel (callback) - "Sentinel function used by KUBEL--EXEC. - -CALLBACK is called when process completes successfully. -" + "Sentinel function used by `kubel--exec-async'. +CALLBACK is called when process completes successfully." (lambda (process event) (let ((process-name (process-name process)) - (exit-status (process-exit-status process))) + (exit-status (process-exit-status process)) + (error-buffer (kubel--process-error-buffer process-name))) (kubel--append-to-process-buffer (format "[%s]\nexit-code: %s" process-name exit-status)) (if (eq 0 exit-status) (when callback (funcall callback)) - (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))))))) + (kubel--handle-stderr (with-current-buffer error-buffer + (buffer-string)))) + ;; Clean up error buffer + (when (get-buffer error-buffer) + (kill-buffer error-buffer))))) -(defun kubel--exec (process-name args &optional readonly callback) - "Utility function to run commands in the proper context and namespace. +(defun kubel--exec-async (process-name args &optional readonly callback) + "Run kubectl asynchronously with ARGS, showing output in a buffer. PROCESS-NAME is an identifier for the process. Default to \"kubel-command\". -ARGS is a ist of arguments. +ARGS is a list of arguments. CALLBACK is a function that will be executed when the command completes. -READONLY If true buffer will be in readonly mode(view-mode)." +READONLY if non-nil, buffer will be in `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 "_"))) @@ -576,7 +592,7 @@ POD-NAME is the name of the pod. TYPE is containers or initContainers." (unless type (setq type "containers")) (split-string - (kubel--exec-to-string + (kubel--exec-sync (format "%s get pod %s -o jsonpath='{.spec.%s[*].name}'" (kubel--get-command-prefix) pod-name type)) " ")) (defun kubel--get-pod-labels () @@ -587,7 +603,7 @@ TYPE is containers or initContainers." (regexp-quote ":") "=" (replace-regexp-in-string "map\\[\\(.+?\\)\\]" "\\1" - (kubel--exec-to-string + (kubel--exec-sync (format "%s get pod -o jsonpath='{.items[*].metadata.labels}'" (kubel--get-command-prefix))))))) (splitted (mapcan (lambda (s) (split-string s ",")) raw-labels)) @@ -602,7 +618,7 @@ 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) " ")))) + (split-string (kubel--exec-sync cmd) " ")))) (defun kubel--describe-resource (name &optional describe) "Describe a specific resource. @@ -615,8 +631,8 @@ DESCRIBE is boolean to describe instead of get resource details" (set-buffer-modified-p nil) (goto-char (point-min))))) (if describe - (kubel--exec process-name (list "describe" name resource) nil callback) - (kubel--exec process-name (list "get" name "-o" kubel-output resource) nil callback)) + (kubel--exec-async process-name (list "describe" name resource) nil callback) + (kubel--exec-async process-name (list "get" name "-o" kubel-output resource) nil callback)) (when (string-equal kubel-output "yaml") (kubel-yaml-editing-mode)))) @@ -629,7 +645,7 @@ NAME is the resource name." (revision (car (split-string (kubel--select-rollout typename)))) (process-name (format "kubel - rollout - %s - %s" typename revision)) (callback (goto-char (point-min)))) - (kubel--exec process-name + (kubel--exec-async process-name (list "rollout" "history" typename (format "--revision=%s" revision)) nil callback))) (defun kubel--list-rollout (typename) @@ -637,7 +653,7 @@ NAME is the resource name." 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" t)))) + (nthcdr 2 (split-string (kubel--exec-sync cmd) "\n" t)))) (defun kubel--select-rollout (typename) "Select a rollout version. @@ -703,7 +719,7 @@ Allows simple apply of the changes made. (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) nil (lambda () (message "Applied %s" filename)))))) + (kubel--exec-async (format "kubectl - apply - %s" filename) (list "apply" "-f" filename-without-tramp-prefix) nil (lambda () (message "Applied %s" filename)))))) (defun kubel-get-resource-details (&optional describe) "Get the details of the resource under the cursor. @@ -719,8 +735,8 @@ Allows simple apply of the changes made. (set-buffer-modified-p nil) (goto-char (point-min))))) (if describe - (kubel--exec process-name (list "describe" kubel-resource (kubel--get-resource-under-cursor)) nil callback) - (kubel--exec process-name (list "get" kubel-resource (kubel--get-resource-under-cursor) "-o" kubel-output) nil callback)) + (kubel--exec-async process-name (list "describe" kubel-resource (kubel--get-resource-under-cursor)) nil callback) + (kubel--exec-async process-name (list "get" kubel-resource (kubel--get-resource-under-cursor) "-o" kubel-output) nil callback)) (when (or (string-equal kubel-output "yaml") (transient-args 'kubel-describe-popup)) (kubel-yaml-editing-mode) (setq kubel-context ctx) @@ -754,7 +770,7 @@ TYPE is containers or initContainers." (car containers) (completing-read "Select container: " containers))) (process-name (format "kubel - logs - %s - %s" pod container))) - (kubel--exec process-name + (kubel--exec-async process-name (append '("logs") (kubel--default-tail-arg args) (list pod container)) t nil)))) (defun kubel-get-pod-logs--initContainer (&optional args) @@ -773,7 +789,7 @@ ARGS is the arguments list from transient." (let* ((labels (kubel--get-pod-labels)) (label (completing-read "Select container: " labels)) (process-name (format "kubel - logs - %s" label))) - (kubel--exec process-name + (kubel--exec-async process-name (append '("logs") (kubel--default-tail-arg args) '("-l") (list label)) t nil))) (defun kubel-copy-resource-name () @@ -822,7 +838,7 @@ ARGS is the arguments list from transient." (unless kubel--can-get-namespace-cached (setq kubel--can-get-namespace-cached (string-match-p "yes\n" - (kubel--exec-to-string + (kubel--exec-sync (format "%s --context %s auth can-i list namespaces" kubel-kubectl kubel-context)))))) kubel--can-get-namespace-cached))) @@ -830,7 +846,7 @@ ARGS is the arguments list from transient." "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 + (split-string (kubel--exec-sync (format "%s --context %s get namespace -o jsonpath='{.items[*].metadata.name}'" kubel-kubectl kubel-context)) " "))) kubel--namespace-list-cached) @@ -871,7 +887,7 @@ the context caches, including the cached resource list." (setq kubel-context (completing-read "Select context: " - (split-string (kubel--exec-to-string (format "%s config view -o jsonpath='{.contexts[*].name}'" kubel-kubectl)) " "))) + (split-string (kubel--exec-sync (format "%s config view -o jsonpath='{.contexts[*].name}'" kubel-kubectl)) " "))) (kubel--invalidate-context-caches) (setq kubel-namespace "default") (switch-to-buffer (current-buffer)) @@ -912,7 +928,7 @@ the context caches, including the cached resource list." (defun kubel--fetch-api-resource-list () "Fetch the API resource list." - (split-string (kubel--exec-to-string + (split-string (kubel--exec-sync (format "%s --context %s api-resources -o name --no-headers=true" kubel-kubectl kubel-context)) "\n" t)) (defun kubel-set-resource (&optional refresh) @@ -949,7 +965,7 @@ P can be a single number or a localhost:container port pair." (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)))) + (kubel--exec-async process-name (list "port-forward" pod port)))) (defun kubel-setup-tramp () "Setup a kubectl TRAMP." @@ -1092,7 +1108,7 @@ the variables `kubel-namespace' and `kubel-context', respectively." (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)))) + (kubel--exec-async process-name args)))) (defun kubel-jab-deployment () "Make a trivial patch to force a new deployment. @@ -1105,7 +1121,7 @@ See https://github.com/kubernetes/kubernetes/issues/27081" (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" + (kubel--exec-async process-name (list "patch" "deployment" deployment "-p" (format "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"%s\"}}}}}" (round (time-to-seconds)))))))) @@ -1117,7 +1133,7 @@ REPLICAS is the number of desired replicas." (if (kubel--is-scalable) (let* ((resource (kubel--get-resource-under-cursor)) (process-name (format "kubel:scale:%s/%s" kubel-resource resource))) - (kubel--exec process-name (list "scale" kubel-resource resource "--replicas" (number-to-string replicas)))) + (kubel--exec-async process-name (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))) @@ -1178,8 +1194,10 @@ RESET is to be called if the search is nil after the first attempt." (defun kubel-show-process-buffer () "Show the kubel-process-buffer." (interactive) - (pop-to-buffer kubel--process-buffer) - (special-mode)) + (with-current-buffer (get-buffer-create kubel--process-buffer) + (special-mode)) + (display-buffer kubel--process-buffer + '(nil (inhibit-same-window . t)))) (defun kubel-mark-item () "Mark or unmark the item under cursor." From a554229e356d010ecb93f6132167788a6df80832 Mon Sep 17 00:00:00 2001 From: Damian Barabonkov Date: Fri, 23 Jan 2026 08:55:39 -0800 Subject: [PATCH 02/14] fix: Improve buffer display behavior in kubel.el --- kubel.el | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/kubel.el b/kubel.el index a8e5325..ed51336 100644 --- a/kubel.el +++ b/kubel.el @@ -1196,8 +1196,10 @@ RESET is to be called if the search is nil after the first attempt." (interactive) (with-current-buffer (get-buffer-create kubel--process-buffer) (special-mode)) - (display-buffer kubel--process-buffer - '(nil (inhibit-same-window . t)))) + (select-window + (display-buffer kubel--process-buffer + '(display-buffer-below-selected + (window-height . 0.4))))) (defun kubel-mark-item () "Mark or unmark the item under cursor." From f72a3a5123efd88172d2789fdd699976cb8313ff Mon Sep 17 00:00:00 2001 From: Damian Barabonkov Date: Fri, 23 Jan 2026 09:07:25 -0800 Subject: [PATCH 03/14] feat: Display error messages in header line overlay --- kubel.el | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/kubel.el b/kubel.el index ed51336..e4013e2 100644 --- a/kubel.el +++ b/kubel.el @@ -293,13 +293,32 @@ CMD is the kubectl command as a list." (kubel--append-to-process-buffer (format "[%s]\ncommand: %s" process-name str-cmd)))) +(defun kubel--set-header-error (error-msg) + "Set ERROR-MSG as an overlay at the top of the buffer." + (kubel--clear-header-error) + (setq kubel--header-error error-msg) + (when error-msg + (let ((ov (make-overlay (point-min) (point-min)))) + (overlay-put ov 'before-string + (propertize (format "⚠ Kubel error: %s [Type `$' for details]\n" error-msg) + 'face 'error)) + (overlay-put ov 'kubel-error t) + (setq kubel--error-overlay ov)))) + +(defun kubel--clear-header-error () + "Clear any error overlay from the buffer." + (when kubel--error-overlay + (delete-overlay kubel--error-overlay) + (setq kubel--error-overlay nil)) + (setq kubel--header-error nil)) + (defun kubel--handle-stderr (stderr-content) - "Handle stderr output by logging to process buffer and displaying to user. + "Handle stderr output by logging to process buffer and displaying in header-line. STDERR-CONTENT is the stderr string from a kubectl command." (when (and stderr-content (not (string-empty-p stderr-content))) (let ((trimmed (string-trim stderr-content))) (kubel--append-to-process-buffer (format "stderr: %s" trimmed)) - (message "Kubel error: %s [Type `$' for details]" trimmed)))) + (kubel--set-header-error trimmed)))) (defun kubel--exec-sync (cmd) "Run CMD synchronously and return output as string. @@ -335,6 +354,12 @@ Logs to process buffer and displays any stderr via `kubel--handle-stderr'." (defvar-local kubel-selector "" "Label selector for resources.") +(defvar-local kubel--header-error nil + "Current error message to display, or nil if no error.") + +(defvar-local kubel--error-overlay nil + "Overlay used to display error message at top of buffer.") + (defvar kubel-namespace-history '() "List of previously used namespaces.") @@ -526,7 +551,9 @@ CALLBACK is called when process completes successfully." (error-buffer (kubel--process-error-buffer process-name))) (kubel--append-to-process-buffer (format "[%s]\nexit-code: %s" process-name exit-status)) (if (eq 0 exit-status) - (when callback (funcall callback)) + (progn + (kubel--clear-header-error) + (when callback (funcall callback))) (kubel--handle-stderr (with-current-buffer error-buffer (buffer-string)))) ;; Clean up error buffer @@ -1395,6 +1422,7 @@ context, namespace, and resource. DIRECTORY is optional for TRAMP support." (interactive) + (kubel--clear-header-error) (when directory (setq default-directory directory)) (let ((name (kubel--buffer-name))) ;; Remove old buffer if exist but not is current buffer From 23ad7ac0cd71af479035a2a5d8bbebb4b6e07a74 Mon Sep 17 00:00:00 2001 From: Damian Barabonkov Date: Fri, 23 Jan 2026 09:12:11 -0800 Subject: [PATCH 04/14] feat: Add global minor mode with keybindings for kubel --- kubel.el | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/kubel.el b/kubel.el index e4013e2..9a2fc27 100644 --- a/kubel.el +++ b/kubel.el @@ -1488,5 +1488,20 @@ DIRECTORY is optional for TRAMP support." (hl-line-mode 1) (run-mode-hooks 'kubel-mode-hook)) +(defvar kubel-global-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-c o K") #'kubel) + map) + "Keymap for `kubel-global-mode'.") + +;;;###autoload +(define-minor-mode kubel-global-mode + "Global minor mode to provide keybindings for kubel. + +\\{kubel-global-mode-map}" + :global t + :group 'kubel + :keymap kubel-global-mode-map) + (provide 'kubel) ;;; kubel.el ends here From 24142f01cae01188061580ee33dddbeb1c6fca0b Mon Sep 17 00:00:00 2001 From: Damian Barabonkov Date: Fri, 23 Jan 2026 09:16:04 -0800 Subject: [PATCH 05/14] refactor: Organize kubel help menu actions for clarity --- kubel.el | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/kubel.el b/kubel.el index 9a2fc27..fcc862c 100644 --- a/kubel.el +++ b/kubel.el @@ -1336,14 +1336,13 @@ When called interactively, prompts for a buffer belonging to kubel." (transient-define-prefix kubel-help-popup () "Kubel Menu" [["Actions" - ;; global ("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 + ["Pod Actions" ("p" "Port forward" kubel-port-forward-pod) ("l" "Logs" kubel-log-popup) ("e" "Exec" kubel-exec-popup) @@ -1354,8 +1353,8 @@ When called interactively, prompts for a buffer belonging to kubel." ("n" "Set namespace" kubel-set-namespace) ("R" "Set resource" kubel-set-resource) ("K" "Set kubectl config file" kubel-set-kubectl-config-file) - ("F" "Set output format" kubel-set-output-format)] - ["Filter" + ("F" "Set output format" kubel-set-output-format)]] + [["Filter" ("f" "Filter" kubel-set-filter) ("M-n" "Next highlight" kubel-jump-to-next-highlight) ("M-p" "Previous highlight" kubel-jump-to-previous-highlight) @@ -1366,7 +1365,7 @@ When called interactively, prompts for a buffer belonging to kubel." ("M" "Mark all items" kubel-mark-all) ("U" "Unmark all items" kubel-unmark-all)] ["Utilities" - ("c" "Copy to clipboad..." kubel-copy-popup) + ("c" "Copy to clipboard..." kubel-copy-popup) ("$" "Show Process buffer" kubel-show-process-buffer)]]) ;; mode map From e8e5e33c6202f29300b8e582da3ac13ccae2d68d Mon Sep 17 00:00:00 2001 From: Damian Barabonkov Date: Fri, 23 Jan 2026 09:23:41 -0800 Subject: [PATCH 06/14] feat: Add command to list all Kubernetes namespaces --- kubel.el | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/kubel.el b/kubel.el index fcc862c..4006b21 100644 --- a/kubel.el +++ b/kubel.el @@ -583,7 +583,7 @@ READONLY if non-nil, buffer will be in `view-mode'." :file-handler t :stderr (get-buffer-create error-buffer) :command cmd) - (pop-to-buffer buffer-name) + (pop-to-buffer-same-window buffer-name) (if readonly (with-current-buffer buffer-name (view-mode))))) @@ -888,6 +888,13 @@ ARGS is the arguments list from transient." (unless (member namespace kubel-namespace-history) (push namespace kubel-namespace-history))) +(defun kubel-list-namespaces () + "List all namespaces in the current context." + (interactive) + (kubel--exec-async "kubel-list-namespaces" + (list "get" "namespaces") + t nil)) + (defun kubel-set-namespace (&optional refresh) "Set the namespace. If called with a prefix argument REFRESH, refreshes @@ -1351,6 +1358,7 @@ When called interactively, prompts for a buffer belonging to kubel." ["Settings" ("C" "Set context" kubel-set-context) ("n" "Set namespace" kubel-set-namespace) + ("y" "List namespaces" kubel-list-namespaces) ("R" "Set resource" kubel-set-resource) ("K" "Set kubectl config file" kubel-set-kubectl-config-file) ("F" "Set output format" kubel-set-output-format)]] @@ -1376,6 +1384,7 @@ When called interactively, prompts for a buffer belonging to kubel." (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 "y") 'kubel-list-namespaces) (define-key map (kbd "g") 'kubel-refresh) (define-key map (kbd "h") 'kubel-help-popup) (define-key map (kbd "?") 'kubel-help-popup) From f1182df40e92611085a51f67e530845798f81ceb Mon Sep 17 00:00:00 2001 From: Damian Barabonkov Date: Sat, 24 Jan 2026 07:32:38 -0800 Subject: [PATCH 07/14] feat: Add error handling and lazy initialization for context --- kubel.el | 49 ++++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/kubel.el b/kubel.el index 4006b21..a2b2a9e 100644 --- a/kubel.el +++ b/kubel.el @@ -283,6 +283,12 @@ This is used by `kubel-kill-buffer'." (defvar-local kubel--last-command nil) +(defvar-local kubel--header-error nil + "Current error message to display, or nil if no error.") + +(defvar-local kubel--error-overlay nil + "Overlay used to display error message at top of buffer.") + (defun kubel--log-command (process-name cmd) "Log the kubectl command to the process buffer. @@ -343,10 +349,8 @@ Logs to process buffer and displays any stderr via `kubel--handle-stderr'." (defvar-local kubel-resource "pods" "Current resource.") -(defvar-local kubel-context - (replace-regexp-in-string - "\n" "" (kubel--exec-sync "kubectl config current-context")) - "Current context. Tries to smart default.") +(defvar-local kubel-context nil + "Current context. Initialized lazily on first use.") (defvar-local kubel-resource-filter "" "Substring filter for resource name.") @@ -354,12 +358,6 @@ Logs to process buffer and displays any stderr via `kubel--handle-stderr'." (defvar-local kubel-selector "" "Label selector for resources.") -(defvar-local kubel--header-error nil - "Current error message to display, or nil if no error.") - -(defvar-local kubel--error-overlay nil - "Overlay used to display error message at top of buffer.") - (defvar kubel-namespace-history '() "List of previously used namespaces.") @@ -546,9 +544,9 @@ NAME is the buffer name." "Sentinel function used by `kubel--exec-async'. CALLBACK is called when process completes successfully." (lambda (process event) - (let ((process-name (process-name process)) - (exit-status (process-exit-status process)) - (error-buffer (kubel--process-error-buffer process-name))) + (let* ((process-name (process-name process)) + (exit-status (process-exit-status process)) + (error-buffer (kubel--process-error-buffer process-name))) (kubel--append-to-process-buffer (format "[%s]\nexit-code: %s" process-name exit-status)) (if (eq 0 exit-status) (progn @@ -762,13 +760,8 @@ Allows simple apply of the changes made. (set-buffer-modified-p nil) (goto-char (point-min))))) (if describe - (kubel--exec-async process-name (list "describe" kubel-resource (kubel--get-resource-under-cursor)) nil callback) - (kubel--exec-async process-name (list "get" kubel-resource (kubel--get-resource-under-cursor) "-o" kubel-output) nil callback)) - (when (or (string-equal kubel-output "yaml") (transient-args 'kubel-describe-popup)) - (kubel-yaml-editing-mode) - (setq kubel-context ctx) - (setq kubel-namespace ns) - (setq kubel-resource res)))) + (kubel--exec-async process-name (list "describe" kubel-resource (kubel--get-resource-under-cursor)) t callback) + (kubel--exec-async process-name (list "get" kubel-resource (kubel--get-resource-under-cursor) "-o" kubel-output) t callback)))) (defun kubel--default-tail-arg (args) "Ugly function to make sure that there is at least the default tail. @@ -891,9 +884,14 @@ ARGS is the arguments list from transient." (defun kubel-list-namespaces () "List all namespaces in the current context." (interactive) - (kubel--exec-async "kubel-list-namespaces" - (list "get" "namespaces") - t nil)) + (let* ((kubel--buffer (get-buffer (kubel--buffer-name))) + (last-default-directory (when kubel--buffer + (with-current-buffer kubel--buffer default-directory)))) + (with-current-buffer (clone-buffer) + (setq kubel-resource "namespaces") + (setq kubel-selector "") + (switch-to-buffer (current-buffer)) + (kubel-refresh last-default-directory)))) (defun kubel-set-namespace (&optional refresh) "Set the namespace. @@ -1483,6 +1481,11 @@ DIRECTORY is optional for TRAMP support." (switch-to-buffer (current-buffer)) (unless (eq major-mode 'kubel-mode) (kubel-mode)) + ;; Lazily initialize context on first use + (unless kubel-context + (setq kubel-context + (replace-regexp-in-string + "\n" "" (kubel--exec-sync "kubectl config current-context")))) (kubel-refresh directory)))) (define-derived-mode kubel-mode tabulated-list-mode "Kubel" From 2c8febae3e82e36c2329037b0911a990cb82d8db Mon Sep 17 00:00:00 2001 From: Damian Barabonkov Date: Sat, 24 Jan 2026 07:48:01 -0800 Subject: [PATCH 08/14] feat: Add back-navigation functionality in kubel buffer --- kubel.el | 52 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/kubel.el b/kubel.el index a2b2a9e..06dad4b 100644 --- a/kubel.el +++ b/kubel.el @@ -374,6 +374,10 @@ Logs to process buffer and displays any stderr via `kubel--handle-stderr'." (defvar-local kubel--kubernetes-resources-list-cached nil) +(defvar-local kubel--parent-buffer nil + "The buffer that spawned this kubel buffer. +Used for back-navigation when pressing q.") + (defun kubel--kubernetes-resources-list () "Get list of resources from cache or from fetching the api resource." (if (null kubel--kubernetes-resources-list-cached) @@ -711,6 +715,20 @@ TYPENAME is the resource type/name." (yes-or-no-p "Resource modified; kill anyway? ")) (kill-buffer (current-buffer)))) +(defun kubel-go-back () + "Kill current buffer and return to parent kubel buffer. +If no parent buffer exists, call `quit-window'." + (interactive) + (let ((parent kubel--parent-buffer)) + (if (and parent (buffer-live-p parent)) + (progn + (kill-buffer (current-buffer)) + (switch-to-buffer parent) + ;; Re-display error overlay if there was an error + (when kubel--header-error + (kubel--set-header-error kubel--header-error))) + (quit-window)))) + (defvar kubel-yaml-editing-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "C-c C-c") #'kubel-apply) @@ -885,9 +903,11 @@ ARGS is the arguments list from transient." "List all namespaces in the current context." (interactive) (let* ((kubel--buffer (get-buffer (kubel--buffer-name))) + (parent-buffer (current-buffer)) (last-default-directory (when kubel--buffer (with-current-buffer kubel--buffer default-directory)))) (with-current-buffer (clone-buffer) + (setq kubel--parent-buffer parent-buffer) (setq kubel-resource "namespaces") (setq kubel-selector "") (switch-to-buffer (current-buffer)) @@ -902,9 +922,11 @@ the context caches, including the cached resource list." (let* ((namespace (completing-read "Namespace: " (kubel--list-namespace) nil nil nil nil "default")) (kubel--buffer (get-buffer (kubel--buffer-name))) + (parent-buffer (current-buffer)) (last-default-directory (when kubel--buffer (with-current-buffer kubel--buffer default-directory)))) (with-current-buffer (clone-buffer) + (setq kubel--parent-buffer parent-buffer) (setq kubel-namespace namespace) (kubel--add-namespace-to-history namespace) (switch-to-buffer (current-buffer)) @@ -914,8 +936,10 @@ the context caches, including the cached resource list." "Set the context." (interactive) (let* ((kubel--buffer (get-buffer (kubel--buffer-name))) + (parent-buffer (current-buffer)) (last-default-directory (when kubel--buffer (with-current-buffer kubel--buffer default-directory)))) (with-current-buffer (clone-buffer) + (setq kubel--parent-buffer parent-buffer) (setq kubel-context (completing-read "Select context: " @@ -946,17 +970,19 @@ the context caches, including the cached resource list." (defun kubel-set-label-selector () "Set the selector." (interactive) - (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))) + (let ((parent-buffer (current-buffer))) + (with-current-buffer (clone-buffer) + (setq kubel--parent-buffer parent-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." @@ -972,8 +998,10 @@ the context caches, including the cached resource list." (let* ((current-buffer-name (kubel--buffer-name)) (resource-list (kubel--kubernetes-resources-list)) (kubel--buffer (get-buffer current-buffer-name)) + (parent-buffer (current-buffer)) (last-default-directory (when kubel--buffer (with-current-buffer kubel--buffer default-directory)))) (with-current-buffer (clone-buffer) + (setq kubel--parent-buffer parent-buffer) (setq kubel-resource (completing-read "Select resource: " resource-list)) (switch-to-buffer (current-buffer)) @@ -1411,6 +1439,8 @@ When called interactively, prompts for a buffer belonging to kubel." (define-key map (kbd "M") 'kubel-mark-all) (define-key map (kbd "U") 'kubel-unmark-all) + (define-key map (kbd "q") 'kubel-go-back) + map) "Keymap for `kubel-mode'.") From a7a6808d604014199133d537a353a47ce5f0254c Mon Sep 17 00:00:00 2001 From: Damian Barabonkov Date: Sat, 24 Jan 2026 08:02:21 -0800 Subject: [PATCH 09/14] feat: Add option to silence stderr warnings and new kill function --- kubel.el | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/kubel.el b/kubel.el index 06dad4b..ee3fe76 100644 --- a/kubel.el +++ b/kubel.el @@ -326,9 +326,10 @@ STDERR-CONTENT is the stderr string from a kubectl command." (kubel--append-to-process-buffer (format "stderr: %s" trimmed)) (kubel--set-header-error trimmed)))) -(defun kubel--exec-sync (cmd) +(defun kubel--exec-sync (cmd &optional silence-warnings) "Run CMD synchronously and return output as string. -Logs to process buffer and displays any stderr via `kubel--handle-stderr'." +Logs to process buffer and displays any stderr via `kubel--handle-stderr'. +If SILENCE-WARNINGS is non-nil, stderr output is not displayed." (kubel--log-command "kubectl-command" cmd) (let ((stderr-buffer (generate-new-buffer " *kubel-stderr-temp*")) (result nil)) @@ -338,8 +339,9 @@ Logs to process buffer and displays any stderr via `kubel--handle-stderr'." (with-output-to-string (with-current-buffer standard-output (shell-command cmd t stderr-buffer)))) - (kubel--handle-stderr (with-current-buffer stderr-buffer - (buffer-string)))) + (unless silence-warnings + (kubel--handle-stderr (with-current-buffer stderr-buffer + (buffer-string))))) (kill-buffer stderr-buffer)) result)) @@ -729,6 +731,13 @@ If no parent buffer exists, call `quit-window'." (kubel--set-header-error kubel--header-error))) (quit-window)))) +(defun kubel-kill-all-buffers () + "Kill all kubel-related buffers." + (interactive) + (dolist (buf (buffer-list)) + (when (string-prefix-p "*kubel:" (buffer-name buf)) + (kill-buffer buf)))) + (defvar kubel-yaml-editing-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "C-c C-c") #'kubel-apply) @@ -875,9 +884,10 @@ ARGS is the arguments list from transient." (progn (unless kubel--can-get-namespace-cached (setq kubel--can-get-namespace-cached - (string-match-p "yes\n" + (string-match-p "yes" (kubel--exec-sync - (format "%s --context %s auth can-i list namespaces" kubel-kubectl kubel-context)))))) + (format "%s --context %s auth can-i list namespaces" kubel-kubectl kubel-context) + t))))) ; silence warnings kubel--can-get-namespace-cached))) (defun kubel--get-namespace () @@ -1440,6 +1450,7 @@ When called interactively, prompts for a buffer belonging to kubel." (define-key map (kbd "U") 'kubel-unmark-all) (define-key map (kbd "q") 'kubel-go-back) + (define-key map (kbd "Q") 'kubel-kill-all-buffers) map) "Keymap for `kubel-mode'.") From 9828530beb1abf860e391b2f3e5bf6dbed7c9553 Mon Sep 17 00:00:00 2001 From: Damian Barabonkov Date: Sat, 24 Jan 2026 08:07:47 -0800 Subject: [PATCH 10/14] feat: Allow namespace selection from marked resources --- kubel.el | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/kubel.el b/kubel.el index ee3fe76..9e71a8e 100644 --- a/kubel.el +++ b/kubel.el @@ -926,11 +926,22 @@ ARGS is the arguments list from transient." (defun kubel-set-namespace (&optional refresh) "Set the namespace. If called with a prefix argument REFRESH, refreshes -the context caches, including the cached resource list." +the context caches, including the cached resource list. +When viewing namespaces, selects the namespace under cursor, +or the marked namespace if exactly one is marked." (interactive "P") (when refresh (kubel--invalidate-context-caches)) - (let* ((namespace (completing-read "Namespace: " (kubel--list-namespace) - nil nil nil nil "default")) + (let* ((from-namespace-list (equal kubel-resource "namespaces")) + (namespace (if from-namespace-list + (cond + ((> (length kubel--selected-items) 1) + (user-error "Cannot set namespace: multiple namespaces are marked")) + ((= (length kubel--selected-items) 1) + (car kubel--selected-items)) + (t + (kubel--get-resource-under-cursor))) + (completing-read "Namespace: " (kubel--list-namespace) + nil nil nil nil "default"))) (kubel--buffer (get-buffer (kubel--buffer-name))) (parent-buffer (current-buffer)) (last-default-directory (when kubel--buffer @@ -938,6 +949,8 @@ the context caches, including the cached resource list." (with-current-buffer (clone-buffer) (setq kubel--parent-buffer parent-buffer) (setq kubel-namespace namespace) + (when from-namespace-list + (setq kubel-resource "pods")) (kubel--add-namespace-to-history namespace) (switch-to-buffer (current-buffer)) (kubel-refresh last-default-directory)))) From 36d7025aa0e1f4c8611e4d1e5ea9f5014f29fff8 Mon Sep 17 00:00:00 2001 From: Damian Barabonkov Date: Sat, 24 Jan 2026 08:31:30 -0800 Subject: [PATCH 11/14] feat: Add custom entry printer for selected items --- kubel.el | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/kubel.el b/kubel.el index 9e71a8e..53ad927 100644 --- a/kubel.el +++ b/kubel.el @@ -522,15 +522,27 @@ If MAX is the end of the line, dynamically adjust." "Return non-nil if there are items selected." (>= (length kubel--selected-items) 1)) +(defun kubel--print-entry (id cols) + "Custom entry printer that applies `dired-marked' face to selected items. +ID is the entry identifier (resource name). +COLS is the vector of column values. +When selected, prepends `*' to NAME and applies `dired-marked' face to entire row." + (let* ((is-selected (-contains? kubel--selected-items id)) + (first-col (aref cols 0)) + (modified-cols (if is-selected + (vconcat (vector (propertize (concat "*" first-col) 'face 'dired-marked)) + (mapcar (lambda (col) (propertize col 'face 'dired-marked)) + (seq-drop cols 1))) + cols))) + (tabulated-list-print-entry id modified-cols))) + (defun kubel--propertize-status (status) "Return the status in proper font color. STATUS is the pod status string." (let ((status-face (cdr (assoc status kubel-status-faces))) - (match (or (equal kubel-resource-filter "") (string-match-p kubel-resource-filter status))) - (selected (and (kubel--items-selected-p) (-contains? kubel--selected-items status)))) + (match (or (equal kubel-resource-filter "") (string-match-p kubel-resource-filter status)))) (cond (status-face (propertize status 'face status-face)) - (selected (propertize (concat "*" status) 'face 'dired-marked)) ((not match) (propertize status 'face 'shadow)) (t status)))) @@ -593,11 +605,9 @@ READONLY if non-nil, buffer will be in `view-mode'." (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" + "Utility function to get the name of the resource under the cursor." (string-remove-suffix " (default)" ;; see https://github.com/abrochard/kubel/issues/106 - (replace-regexp-in-string - "^\*" "" (aref (tabulated-list-get-entry) 0)))) + (aref (tabulated-list-get-entry) 0))) (defun kubel--get-context-namespace () "Utility function to return the proper context and namespace arguments." @@ -1285,23 +1295,23 @@ RESET is to be called if the search is nil after the first attempt." (window-height . 0.4))))) (defun kubel-mark-item () - "Mark or unmark the item under cursor." + "Mark the item under cursor and move to next line." (interactive) - (let ((item (kubel--get-resource-under-cursor))) + (let ((item (kubel--get-resource-under-cursor)) + (col (current-column))) (unless (-contains? kubel--selected-items item) - (progn - (push item kubel--selected-items) - (forward-line 1) - (kubel-refresh))))) + (push item kubel--selected-items) + (forward-line 1) + (move-to-column col) + (tabulated-list-print t)))) (defun kubel-unmark-item () "Unmark the item under cursor." (interactive) (let ((item (kubel--get-resource-under-cursor))) (when (-contains? kubel--selected-items item) - (progn - (setq kubel--selected-items (delete item kubel--selected-items)) - (kubel-refresh))))) + (setq kubel--selected-items (delete item kubel--selected-items)) + (tabulated-list-print t)))) (defun kubel-mark-all () "Mark all items." @@ -1312,13 +1322,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-refresh)) + (tabulated-list-print t)) (defun kubel-unmark-all () "Unmark all items." (interactive) (setq kubel--selected-items '()) - (kubel-refresh)) + (tabulated-list-print t)) (defun kubel--read-buffer () "Return the list of all buffers of kubel pattern." @@ -1549,6 +1559,7 @@ DIRECTORY is optional for TRAMP support." (setq truncate-lines t) (setq mode-name "Kubel") (setq major-mode 'kubel-mode) + (setq-local tabulated-list-printer #'kubel--print-entry) (use-local-map kubel-mode-map) (hl-line-mode 1) (run-mode-hooks 'kubel-mode-hook)) From 375df180b9c40d3616b888bd7d4f0851edadcca6 Mon Sep 17 00:00:00 2001 From: Damian Barabonkov Date: Sat, 24 Jan 2026 08:50:29 -0800 Subject: [PATCH 12/14] feat: Add support for setting default namespace per context --- kubel.el | 52 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/kubel.el b/kubel.el index 53ad927..2d1610a 100644 --- a/kubel.el +++ b/kubel.el @@ -370,6 +370,9 @@ If SILENCE-WARNINGS is non-nil, stderr output is not displayed." (defvar kubel--namespace-list-cached nil) +(defvar kubel--context-namespace-cache (make-hash-table :test 'equal) + "Cache mapping context names to their configured default namespace.") + (defvar-local kubel--label-values-cached nil) (defvar-local kubel--selected-items '()) @@ -392,7 +395,24 @@ Used for back-navigation when pressing q.") (setq kubel--kubernetes-resources-list-cached nil) (setq kubel--can-get-namespace-cached nil) (setq kubel--namespace-list-cached nil) - (setq kubel--label-values-cached nil)) + (setq kubel--label-values-cached nil) + (clrhash kubel--context-namespace-cache)) + +(defun kubel--get-context-default-namespace (&optional context) + "Get the default namespace for CONTEXT from kubectl config. +If CONTEXT is nil, uses `kubel-context'. +Falls back to `kubel-default-namespace' if none is configured. +Results are cached in `kubel--context-namespace-cache'." + (let ((ctx (or context kubel-context))) + (or (gethash ctx kubel--context-namespace-cache) + (let* ((cmd (if ctx + (format "%s config view --minify -o jsonpath={..namespace} --context=%s" + kubel-kubectl ctx) + (format "%s config view --minify -o jsonpath={..namespace}" + kubel-kubectl))) + (result (string-trim (shell-command-to-string cmd)))) + (puthash ctx (if (string-empty-p result) kubel-default-namespace result) + kubel--context-namespace-cache))))) (defun kubel--populate-list () "Return a list with a tabulated list format and \"tabulated-list-entries\"." @@ -978,10 +998,32 @@ or the marked namespace if exactly one is marked." "Select context: " (split-string (kubel--exec-sync (format "%s config view -o jsonpath='{.contexts[*].name}'" kubel-kubectl)) " "))) (kubel--invalidate-context-caches) - (setq kubel-namespace "default") + (setq kubel-namespace (kubel--get-context-default-namespace kubel-context)) (switch-to-buffer (current-buffer)) (kubel-refresh last-default-directory)))) +(defun kubel-set-context-default-namespace () + "Set the default namespace for the current context in kubectl config. +When viewing namespaces, uses the namespace under cursor or marked namespace. +Otherwise, prompts with completing-read." + (interactive) + (let* ((from-namespace-list (equal kubel-resource "namespaces")) + (namespace (if from-namespace-list + (cond + ((> (length kubel--selected-items) 1) + (user-error "Cannot set default namespace: multiple namespaces are marked")) + ((= (length kubel--selected-items) 1) + (car kubel--selected-items)) + (t + (kubel--get-resource-under-cursor))) + (completing-read "Set default namespace: " (kubel--list-namespace) + nil nil nil nil kubel-namespace))) + (cmd (format "%s config set-context %s --namespace=%s" + kubel-kubectl kubel-context namespace))) + (shell-command-to-string cmd) + (remhash kubel-context kubel--context-namespace-cache) + (message "Set default namespace for context %s to %s" kubel-context namespace))) + (defun kubel--add-selector-to-history (selector) "Add SELECTOR to history if it isn't there already." (unless (member selector kubel-selector-history) @@ -1443,6 +1485,7 @@ When called interactively, prompts for a buffer belonging to kubel." (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 "N") 'kubel-set-context-default-namespace) (define-key map (kbd "y") 'kubel-list-namespaces) (define-key map (kbd "g") 'kubel-refresh) (define-key map (kbd "h") 'kubel-help-popup) @@ -1545,11 +1588,12 @@ DIRECTORY is optional for TRAMP support." (switch-to-buffer (current-buffer)) (unless (eq major-mode 'kubel-mode) (kubel-mode)) - ;; Lazily initialize context on first use + ;; Lazily initialize context and namespace on first use (unless kubel-context (setq kubel-context (replace-regexp-in-string - "\n" "" (kubel--exec-sync "kubectl config current-context")))) + "\n" "" (kubel--exec-sync "kubectl config current-context"))) + (setq kubel-namespace (kubel--get-context-default-namespace kubel-context))) (kubel-refresh directory)))) (define-derived-mode kubel-mode tabulated-list-mode "Kubel" From bc8ff7f25d7552dba72e09f828533f6a3e33c7e1 Mon Sep 17 00:00:00 2001 From: Damian Barabonkov Date: Sat, 24 Jan 2026 08:52:54 -0800 Subject: [PATCH 13/14] refactor: Enhance resource name extraction utility functions --- kubel.el | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/kubel.el b/kubel.el index 2d1610a..bcd37bb 100644 --- a/kubel.el +++ b/kubel.el @@ -625,9 +625,11 @@ READONLY if non-nil, buffer will be in `view-mode'." (view-mode))))) (defun kubel--get-resource-under-cursor () - "Utility function to get the name of the resource under the cursor." + "Utility function to get the name of the resource under the cursor. +Strips leading `*' mark indicator if present." (string-remove-suffix " (default)" ;; see https://github.com/abrochard/kubel/issues/106 - (aref (tabulated-list-get-entry) 0))) + (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." @@ -1348,11 +1350,14 @@ RESET is to be called if the search is nil after the first attempt." (tabulated-list-print t)))) (defun kubel-unmark-item () - "Unmark the item under cursor." + "Unmark the item under cursor and move to next line." (interactive) - (let ((item (kubel--get-resource-under-cursor))) + (let ((item (kubel--get-resource-under-cursor)) + (col (current-column))) (when (-contains? kubel--selected-items item) (setq kubel--selected-items (delete item kubel--selected-items)) + (forward-line 1) + (move-to-column col) (tabulated-list-print t)))) (defun kubel-mark-all () From 6f50fa36d7de33421bde289056069a9844d9ee37 Mon Sep 17 00:00:00 2001 From: Damian Barabonkov Date: Sat, 24 Jan 2026 09:14:11 -0800 Subject: [PATCH 14/14] feat: Add GKE authentication handling with user prompt --- kubel.el | 44 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/kubel.el b/kubel.el index bcd37bb..5d81865 100644 --- a/kubel.el +++ b/kubel.el @@ -326,10 +326,44 @@ STDERR-CONTENT is the stderr string from a kubectl command." (kubel--append-to-process-buffer (format "stderr: %s" trimmed)) (kubel--set-header-error trimmed)))) +(defun kubel--gke-auth-error-p (output) + "Return non-nil if OUTPUT indicates a GKE authentication error." + (and output + (string-match-p "gke-gcloud-auth-plugin failed" output))) + +(defun kubel--prompt-gcloud-login () + "Prompt user to run gcloud auth login and execute if confirmed. +Auto-refreshes kubel on successful authentication. +Shows output buffer only on error." + (when (y-or-n-p "GKE authentication expired. Run `gcloud auth login'? ") + (let ((kubel-buffer (current-buffer)) + (output-buffer (get-buffer-create "*gcloud-auth*"))) + (with-current-buffer output-buffer + (erase-buffer)) + (message "Running gcloud auth login... Complete authentication in your browser.") + (make-process + :name "gcloud-auth" + :buffer output-buffer + :command '("gcloud" "auth" "login") + :sentinel (lambda (proc _event) + (let ((exit-code (process-exit-status proc)) + (proc-buffer (process-buffer proc))) + (if (= exit-code 0) + (progn + (message "GKE authentication successful, refreshing kubel...") + (when (buffer-live-p kubel-buffer) + (with-current-buffer kubel-buffer + (kubel-refresh)))) + (progn + (message "GKE authentication failed (exit code %d)" exit-code) + (when (buffer-live-p proc-buffer) + (display-buffer proc-buffer)))))))))) + (defun kubel--exec-sync (cmd &optional silence-warnings) "Run CMD synchronously and return output as string. Logs to process buffer and displays any stderr via `kubel--handle-stderr'. -If SILENCE-WARNINGS is non-nil, stderr output is not displayed." +If SILENCE-WARNINGS is non-nil, stderr output is not displayed. +Detects GKE authentication errors and prompts to re-authenticate." (kubel--log-command "kubectl-command" cmd) (let ((stderr-buffer (generate-new-buffer " *kubel-stderr-temp*")) (result nil)) @@ -339,9 +373,11 @@ If SILENCE-WARNINGS is non-nil, stderr output is not displayed." (with-output-to-string (with-current-buffer standard-output (shell-command cmd t stderr-buffer)))) - (unless silence-warnings - (kubel--handle-stderr (with-current-buffer stderr-buffer - (buffer-string))))) + (let ((stderr-output (with-current-buffer stderr-buffer (buffer-string)))) + (when (kubel--gke-auth-error-p stderr-output) + (kubel--prompt-gcloud-login)) + (unless silence-warnings + (kubel--handle-stderr stderr-output)))) (kill-buffer stderr-buffer)) result))