diff --git a/kubel.el b/kubel.el index 5579606..5d81865 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. @@ -293,14 +299,87 @@ 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--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 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)) + (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. +Detects GKE authentication errors and prompts to re-authenticate." (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)))) + (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)) (defvar-local kubel-namespace kubel-default-namespace "Current namespace.") @@ -308,10 +387,8 @@ CMD is the command string to run." (defvar-local kubel-resource "pods" "Current resource.") -(defvar-local kubel-context - (replace-regexp-in-string - "\n" "" (kubel--exec-to-string "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.") @@ -329,12 +406,19 @@ CMD is the command string to run." (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 '()) (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) @@ -347,11 +431,28 @@ CMD is the command string to run." (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\"." - (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 @@ -477,15 +578,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)))) @@ -502,28 +615,30 @@ 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))) + (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) - (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))))))) - -(defun kubel--exec (process-name args &optional readonly callback) - "Utility function to run commands in the proper context and namespace. + (progn + (kubel--clear-header-error) + (when callback (funcall callback))) + (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-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 "_"))) @@ -540,17 +655,17 @@ READONLY If true buffer will be in readonly mode(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))))) (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" +Strips leading `*' mark indicator if present." (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." @@ -576,7 +691,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 +702,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 +717,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 +730,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 +744,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 +752,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. @@ -670,6 +785,27 @@ 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)))) + +(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) @@ -703,7 +839,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,13 +855,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)) - (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. @@ -754,7 +885,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 +904,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 () @@ -821,16 +952,17 @@ 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" - (kubel--exec-to-string - (format "%s --context %s auth can-i list namespaces" kubel-kubectl kubel-context)))))) + (string-match-p "yes" + (kubel--exec-sync + (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 () "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) @@ -845,19 +977,48 @@ 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) + (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)) + (kubel-refresh last-default-directory)))) + (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 (with-current-buffer kubel--buffer default-directory)))) (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)))) @@ -866,17 +1027,41 @@ 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: " - (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") + (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) @@ -898,21 +1083,23 @@ 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." - (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) @@ -924,8 +1111,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)) @@ -949,7 +1138,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 +1281,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 +1294,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 +1306,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,27 +1367,34 @@ 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)) + (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." + "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." + "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) - (progn - (setq kubel--selected-items (delete item kubel--selected-items)) - (kubel-refresh))))) + (setq kubel--selected-items (delete item kubel--selected-items)) + (forward-line 1) + (move-to-column col) + (tabulated-list-print t)))) (defun kubel-mark-all () "Mark all items." @@ -1209,13 +1405,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." @@ -1289,14 +1485,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) @@ -1305,10 +1500,11 @@ 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)] - ["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) @@ -1319,7 +1515,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 @@ -1330,6 +1526,8 @@ 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) (define-key map (kbd "?") 'kubel-help-popup) @@ -1358,6 +1556,9 @@ 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) + (define-key map (kbd "Q") 'kubel-kill-all-buffers) + map) "Keymap for `kubel-mode'.") @@ -1375,6 +1576,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 @@ -1427,6 +1629,12 @@ DIRECTORY is optional for TRAMP support." (switch-to-buffer (current-buffer)) (unless (eq major-mode 'kubel-mode) (kubel-mode)) + ;; 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"))) + (setq kubel-namespace (kubel--get-context-default-namespace kubel-context))) (kubel-refresh directory)))) (define-derived-mode kubel-mode tabulated-list-mode "Kubel" @@ -1436,9 +1644,25 @@ 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)) +(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