From: Andreas Gerler Date: Sun, 26 Apr 2026 17:57:04 +0000 (+0200) Subject: update kubel X-Git-Url: http://git.bundesbrandschatzamt.de/?a=commitdiff_plain;h=fa7215a4988c58d0a50bb4e48fc4c7709903a29b;p=emacsconfig.git update kubel --- diff --git a/elpa/kubel-20250713.1542/kubel-autoloads.el b/elpa/kubel-20250713.1542/kubel-autoloads.el deleted file mode 100644 index 36e78041..00000000 --- a/elpa/kubel-20250713.1542/kubel-autoloads.el +++ /dev/null @@ -1,55 +0,0 @@ -;;; kubel-autoloads.el --- automatically extracted autoloads (do not edit) -*- lexical-binding: t -*- -;; Generated by the `loaddefs-generate' function. - -;; This file is part of GNU Emacs. - -;;; Code: - -(add-to-list 'load-path (or (and load-file-name (directory-file-name (file-name-directory load-file-name))) (car load-path))) - - - -;;; Generated autoloads from kubel.el - -(autoload 'kubel-yaml-editing-mode "kubel" "\ -Kubel Yaml editing mode. - -Allows simple apply of the changes made. - -\\{kubel-yaml-editing-mode-map} - -(fn)" t) -(autoload 'kubel-vterm-setup "kubel" "\ -Adds a vterm enty to the KUBEL-EXEC-POP.") -(autoload 'kubel-refresh "kubel" "\ -Refresh the current kubel buffer, calling kubectl using the configured -context, namespace, and resource. - -DIRECTORY is optional for TRAMP support. - -(fn &optional DIRECTORY)" t) -(autoload 'kubel-open "kubel" "\ -Create a new kubel buffer using passed parameters CONTEXT NAMESPACE RESOURCE. -DIRECTORY is optional for TRAMP support. - -(fn CONTEXT NAMESPACE RESOURCE &optional DIRECTORY)") -(autoload 'kubel "kubel" "\ -Invoke the kubel buffer. -DIRECTORY is optional for TRAMP support. - -(fn &optional DIRECTORY)" t) -(register-definition-prefixes "kubel" '("kubel-")) - -;;; End of scraped data - -(provide 'kubel-autoloads) - -;; Local Variables: -;; version-control: never -;; no-byte-compile: t -;; no-update-autoloads: t -;; no-native-compile: t -;; coding: utf-8-emacs-unix -;; End: - -;;; kubel-autoloads.el ends here diff --git a/elpa/kubel-20250713.1542/kubel-pkg.el b/elpa/kubel-20250713.1542/kubel-pkg.el deleted file mode 100644 index c1c9bf6c..00000000 --- a/elpa/kubel-20250713.1542/kubel-pkg.el +++ /dev/null @@ -1,12 +0,0 @@ -;; -*- no-byte-compile: t; lexical-binding: nil -*- -(define-package "kubel" "20250713.1542" - "Control Kubernetes with limited permissions." - '((transient "0.1.0") - (emacs "25.3") - (dash "2.12.0") - (s "1.2.0") - (yaml-mode "0.0.14")) - :url "https://github.com/abrochard/kubel" - :commit "9218d91b1085a4cdb240377c335142909df6a9e3" - :revdesc "9218d91b1085" - :keywords '("kubernetes" "k8s" "tools" "processes")) diff --git a/elpa/kubel-20250713.1542/kubel.el b/elpa/kubel-20250713.1542/kubel.el deleted file mode 100644 index bc8c4064..00000000 --- a/elpa/kubel-20250713.1542/kubel.el +++ /dev/null @@ -1,1439 +0,0 @@ -;;; kubel.el --- Control Kubernetes with limited permissions -*- lexical-binding: t; -*- - -;; Copyright (C) 2018, Adrien Brochard - -;; This file is NOT part of Emacs. - -;; This program is free software; you can redistribute it and/or -;; modify it under the terms of the GNU General Public License as -;; published by the Free Software Foundation; either version 2 of the -;; License, or (at your option) any later version. - - - -;; This program is distributed in the hope that it will be useful, but -;; WITHOUT ANY WARRANTY; without even the implied warranty of -;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -;; General Public License for more details. - -;; You should have received a copy of the GNU General Public License -;; along with this program; if not, write to the Free Software -;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 -;; USA - -;; Package-Version: 20250713.1542 -;; Package-Revision: 9218d91b1085 -;; Author: Adrien Brochard -;; Keywords: kubernetes k8s tools processes -;; URL: https://github.com/abrochard/kubel -;; License: GNU General Public License >= 3 -;; Package-Requires: ((transient "0.1.0") (emacs "25.3") (dash "2.12.0") (s "1.2.0") (yaml-mode "0.0.14")) - -;;; Commentary: - -;; Emacs extension for controlling Kubernetes with limited permissions. - -;;; Usage: - -;; To list the pods in your current context and namespace, call -;; -;; M-x kubel -;; -;; To set the path to the kubectl config file, call: -;; M-x kubel-set-kubectl-config-file -;; -;; or -;; -;; (kubel-set-kubectl-config-file ) -;; ex: (kubel-set-kubectl-config-file "~/.kube/another-config") -;; -;; To set said namespace and context, respectively call -;; -;; M-x kubel-set-namespace -;; M-x kubel-set-context -;; -;; Note that namespace will autocomplete but not context, -;; this is because I interact with kubernetes through a user who -;; does not have permissions to list namespaces. -;; -;; To switch to showing a different resource, use the `R` command or -;; -;; M-x kubel-set-resource -;; -;; This will let you select a resource and re-display the kubel buffer. - -;;; Shortcuts: - -;; On the kubel screen, place your cursor on a resource -;; -;; enter => get resource details -;; C-u enter => describe resource -;; h => help popup -;; ? => help popup -;; E => quick edit any resource -;; g => refresh -;; k => delete popup -;; r => see the rollout history for resource -;; p => port forward pod -;; l => log popup -;; e => exec popup -;; j => jab deployment to force rolling update -;; S => scale replicas -;; C => set context -;; n => set namespace -;; R => set resource -;; K => set kubectl config file -;; F => set output format -;; f => set a substring filter for resource name -;; M-n => jump to the next highlighted resource -;; M-p => jump to previous highlighted resource -;; m => mark item -;; u => unmark item -;; M => mark all items -;; U => unmark all items -;; c => copy popup -;; $ => show process buffer -;; s => show only resources with specified label value - -;;; Customize: - -;; By default, kubel log tails from the last 100 lines, you can change the `kubel-log-tail-n` variable to set another line number. - -;;; Code: - -(require 'transient) -(require 'dash) -(require 's) -(require 'yaml-mode) -(require 'tramp) -(require 'subr-x) -(require 'eshell) -(require 'dired) -(require 'json) - -(defgroup kubel nil "Customisation group for kubel." - :group 'extensions) - -(defconst kubel--list-sort-key - '("NAME" . nil) - "Sort table on this key.") - -(defface kubel-status-running - '((default . (:inherit success))) - "The face to use for the Running status.") - -(defface kubel-status-healthy - '((default . (:inherit success))) - "The face to use for the Healthy status.") - -(defface kubel-status-active - '((default . (:inherit success))) - "The face to use for the Active status.") - -(defface kubel-status-ready - '((default . (:inherit success))) - "The face to use for the Ready status.") - -(defface kubel-status-true - '((default . (:inherit success))) - "The face to use for the True status.") - -(defface kubel-status-unknown - '((default . (:inherit warning))) - "The face to use for the Unknown status.") - -(defface kubel-status-error - '((default . (:inherit error))) - "The face to use for the Error status.") - -(defface kubel-status-evicted - '((default . (:inherit error))) - "The face to use for the Evicted status.") - -(defface kubel-status-memory-pressure - '((default . (:inherit error))) - "The face to use for the Memory Pressure status.") - -(defface kubel-status-pid-pressure - '((default . (:inherit error))) - "The face to use for the PID Pressure status.") - -(defface kubel-status-disk-pressure - '((default . (:inherit error))) - "The face to use for the Disk Pressure status.") - -(defface kubel-status-revision-missing - '((default . (:inherit error))) - "The face to use for the Revision Missing status.") - -(defface kubel-status-revision-failed - '((default . (:inherit error))) - "The face to use for the Revision Failed status.") - -(defface kubel-status-network-unavailable - '((default . (:inherit error))) - "The face to use for the Network Unavailable status.") - -(defface kubel-status-completed - '((default . (:foreground "yellow"))) - "The face to use for the Completed status.") - -(defface kubel-status-crash-loop-backoff - '((default . (:inherit error))) - "The face to use for the Crash Loop Backoff status.") - -(defface kubel-status-terminating - '((default . (:foreground "blue"))) - "The face to use for the Terminating status.") - -(defcustom kubel-status-faces - '(("Running" . kubel-status-running) - ("Healthy" . kubel-status-healthy) - ("Active" . kubel-status-active) - ("Ready" . kubel-status-ready) - ("True" . kubel-status-true) - ("Unknown" . kubel-status-unknown) - ("Error" . kubel-status-error) - ("Evicted" . kubel-status-evicted) - ("MemoryPressure" . kubel-status-memory-pressure) - ("PIDPressure" . kubel-status-pid-pressure) - ("DiskPressure" . kubel-status-disk-pressure) - ("RevisionMissing" . kubel-status-revision-missing) - ("RevisionFailed" . kubel-status-revision-failed) - ("NetworkUnavailable" . kubel-status-network-unavailable) - ("Completed" . kubel-status-completed) - ("CrashLoopBackOff" . kubel-status-crash-loop-backoff) - ("Terminating" . kubel-status-terminating)) - "Associative list of status to face." - :type '(alist :key-type string - :value-type face) - :group 'kubel) - -(defcustom kubel-kubectl "kubectl" - "Kubectl binary path." - :type '(file :must-match t) - :group 'kubel) - -(defconst kubel--process-buffer "*kubel-process*" - "Kubel process buffer name.") - -(defcustom kubel-output "yaml" - "Format for output: json|yaml|wide|custom-columns=..." - :type 'string - :group 'kubel) - -(defcustom kubel-log-tail-n 100 - "Default number of lines to tail." - :type 'integer - :group 'kubel) - -(defcustom kubel-list-wide nil - "Control whether list views show additional colums. - -true - use '-o wide' for list views to show additional columns -false - do not use '-o wide' for list views, hiding additional columns" - :type 'boolean - :group 'kubel) - - -(defcustom kubel-use-namespace-list 'auto - "Control behavior for namespace completion. - -auto - default, use `kubectl auth can-i list namespace` to determine if we can - list namespaces -on - always assume we can list namespaces -off - always assume we cannot list namespaces" - :type '(choice (const :tag "Auto" auto) - (const :tag "On" on) - (const :tag "Off" off)) - :group 'kubel) - -(defcustom kubel-shell-buffer-name-format "kubel:%C:%n:%t:%c@%p" - "Define the name format used for pod shell buffers. - -This is a format string with %-sequences that will be substituted -with information about the shell's connection. The following -%-sequences are defined: - -%t: The shell type. Examples of this are `shell` and `eshell` -%c: The container name -%p: The pod name -%n: The current namespace -%C: The current context" - :type 'string - :group 'kubel) - -(defcustom kubel-kill-buffer-query t - "Non-nil means that killing a modified resource buffer has to be confirmed. -This is used by `kubel-kill-buffer'." - :type 'boolean - :group 'kubel) - -(defun kubel--append-to-process-buffer (str) - "Append string STR to the process buffer." - (with-current-buffer (get-buffer-create kubel--process-buffer) - (read-only-mode -1) - (goto-char (point-max)) - (insert (format "%s\n" str)))) - -(defvar-local kubel--last-command nil) - -(defun kubel--log-command (process-name cmd) - "Log the kubectl command to the process buffer. - -PROCESS-NAME is the name of the process. -CMD is the kubectl command as a list." - (let ((str-cmd (if (equal 'string (type-of cmd)) cmd (mapconcat #'identity cmd " ")))) - (setq kubel--last-command str-cmd) - (kubel--append-to-process-buffer - (format "[%s]\ncommand: %s" process-name str-cmd)))) - -(defun kubel--exec-to-string (cmd) - "Replace \"shell-command-to-string\" to log to process buffer. - -CMD is the command string to run." - (kubel--log-command "kubectl-command" cmd) - (with-output-to-string - (with-current-buffer standard-output - (shell-command cmd t "*kubel stderr*")))) - -(defvar-local kubel-namespace "default" - "Current namespace.") - -(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-resource-filter "" - "Substring filter for resource name.") - -(defvar-local kubel-selector "" - "Label selector for resources.") - -(defvar kubel-namespace-history '() - "List of previously used namespaces.") - -(defvar kubel-selector-history '() - "List of previously used selectors.") - -(defvar-local kubel--can-get-namespace-cached nil) - -(defvar kubel--namespace-list-cached nil) - -(defvar-local kubel--label-values-cached nil) - -(defvar-local kubel--selected-items '()) - -(defvar-local kubel--kubernetes-resources-list-cached nil) - -(defun kubel--kubernetes-resources-list () - "Get list of resources from cache or from fetching the api resource." - (if (null kubel--kubernetes-resources-list-cached) - (setq kubel--kubernetes-resources-list-cached - (kubel--fetch-api-resource-list)) - kubel--kubernetes-resources-list-cached)) - -(defun kubel--invalidate-context-caches () - "Invalidate the context caches." - (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)) - -(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" "")))) - (entrylist (kubel--parse-body body))) - (when (string-prefix-p "No resources found" body) - (message "No resources found")) ;; TODO exception here - (list (kubel--get-list-format entrylist) (kubel--get-list-entries entrylist)))) - -(defun kubel--age-to-secs (age) - "Convert AGE in format 1d2h3m4s to seconds." - (let ((rex (rx bol - (opt (group (one-or-more digit)) "d") - (opt (group (one-or-more digit)) "h") - (opt (group (one-or-more digit)) "m") - (opt (group (one-or-more digit)) "s") - eol))) - (if (string-match rex age) - (-sum (--map-indexed - (* (--if-let (match-string (1+ it-index) age) - (string-to-number it) - 0) - it) - '(86400 3600 60 1))) - 0))) - -(defun kubel--make-age-comparator (colnum) - "Return a function that compares two ages at given column COLNUM." - (lambda (row1 row2) - (let ((age1 (elt (cadr row1) colnum)) - (age2 (elt (cadr row2) colnum))) - (< (kubel--age-to-secs age1) - (kubel--age-to-secs age2))))) - -(defun kubel--column-entry (entrylist) - "Return a function of colnum to retrieve an entry in a given column for ENTRYLIST." - (function - (lambda (colnum) - (let* ((name (kubel--column-header entrylist colnum)) - (width (+ 4 (kubel--column-width entrylist colnum))) - (sort (if (member name '("AGE" "DURATION" "LAST SCHEDULE")) - (kubel--make-age-comparator colnum) - t))) - (list name width sort))))) - -(defun kubel--get-list-format (entrylist) - "Get the list format. - -ENTRYLIST is the output of the parsed body." - (defun kubel--get-column-entry (colnum) - (let ((kubel--get-entry (kubel--column-entry entrylist))) - (funcall kubel--get-entry colnum))) - (cl-map 'vector #'kubel--get-column-entry (number-sequence 0 (- (kubel--ncols entrylist) 1)))) - -(defun kubel--update-selected-items (entries) - "Check that all selected items still exist. - -ENTRIES are all resources." - (dolist (i (-difference kubel--selected-items (mapcar #'car entries))) - (setq kubel--selected-items (delete i kubel--selected-items)))) - -(defun kubel--get-list-entries (entrylist) - "Get the entries. - -ENTRYLIST is the output of the parsed body." - (let ((entries (cdr entrylist))) - (kubel--update-selected-items entries) - (mapcar (lambda (x) - (list (car x) - (vconcat [] (mapcar #'kubel--propertize-status x)))) - entries))) - -(defun kubel--parse-body (body) - "Parse the body of kubectl get resource call into a list. - -BODY is the raw output of kubectl get resource." - (let* ((lines (or (nbutlast (split-string body "\n")) '(""))) - (header (car lines)) - ;; Cronjobs have a "LAST SCHEDULE" column, so need to split on 2+ whitespace chars. - (starts (cl-loop for start = 0 then (match-end 0) - while (string-match (rx (>= 2 whitespace)) header start) - collect (match-end 0))) - (position (-zip-with 'cons (cons 0 starts) (append starts '("end")))) - (parse-line (lambda (line) - (mapcar (lambda (pos) - (kubel--extract-value line (car pos) (cdr pos))) - position)))) - (mapcar parse-line lines))) - -(defun kubel--extract-value (line min max) - "Extract value from LINE between MIN and MAX. -If it's just white space, return -, else trim space. -If MAX is the end of the line, dynamically adjust." - (let* ((maxx (if (equal max "end") (length line) max)) - (str (substring-no-properties line min maxx))) - (if (string-match "^ +$" str) - "-" - (string-trim str)))) - -(defun kubel--ncols (entrylist) - "Return the number of columns in ENTRYLIST." - (length (car entrylist))) - -(defun kubel--nrows (entrylist) - "Return the nubmer of rows in ENTRYLIST." - (length entrylist)) - -(defun kubel--column-header (entrylist colnum) - "Return the header for a specific COLNUM in ENTRYLIST." - (nth colnum (car entrylist))) - -(defun kubel--column-width (entrylist colnum) - "Return the width of a specific COLNUM in ENTRYLIST." - (seq-max (mapcar (lambda (x) (length (nth colnum x) )) entrylist))) - -(defun kubel--buffer-name-from-parameters (context namespace resource) - "Return a preconfigured kubel buffer name." - (concat (format "*kubel:%s:%s:%s*" context namespace resource))) - -(defun kubel--buffer-name () - "Return kubel buffer name." - (concat (kubel--buffer-name-from-parameters kubel-context kubel-namespace kubel-resource) - (unless (equal kubel-selector "") - (format " (%s)" kubel-selector)))) - -(defun kubel--items-selected-p () - "Return non-nil if there are items selected." - (>= (length kubel--selected-items) 1)) - -(defun kubel--propertize-status (status) - "Return the status in proper font color. - -STATUS is the pod status string." - (let ((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)))) - (cond (status-face (propertize status 'face status-face)) - (selected (propertize (concat "*" status) 'face 'dired-marked)) - ((not match) (propertize status 'face 'shadow)) - (t status)))) - -(defun kubel--pop-to-buffer (name) - "Utility function to pop to buffer or create it. - -NAME is the buffer name." - (unless (get-buffer name) - (get-buffer-create name)) - (pop-to-buffer-same-window name)) - -(defun kubel--process-error-buffer (process-name) - "Return the error buffer name for the PROCESS-NAME." - (format "*%s:err*" process-name)) - -(defun kubel--sentinel (callback) - "Sentinel function used by KUBEL--EXEC. - -CALLBACK is called when process completes successfully. -" - (lambda (process event) - (let ((process-name (process-name process)) - (exit-status (process-exit-status process))) - (kubel--append-to-process-buffer (format "[%s]\nexit-code: %s" process-name exit-status)) - (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. - -PROCESS-NAME is an identifier for the process. Default to \"kubel-command\". -ARGS is a ist 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)." - (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 "_"))) - (error-buffer (kubel--process-error-buffer process-name)) - (cmd (append (list kubel-kubectl) (kubel--get-context-namespace) args))) - (when (get-buffer buffer-name) - (kill-buffer buffer-name)) - (when (get-buffer error-buffer) - (kill-buffer error-buffer)) - (kubel--log-command process-name cmd) - (make-process :name process-name - :buffer buffer-name - :sentinel (kubel--sentinel callback) - :file-handler t - :stderr (get-buffer-create error-buffer) - :command cmd) - (pop-to-buffer buffer-name) - (if readonly - (with-current-buffer buffer-name - (view-mode))))) - -(defun kubel--get-resource-under-cursor () - "Utility function to get the name of the resource under the cursor. -Strip the `*` prefix if the resource is selected" - (string-remove-suffix " (default)" ;; see https://github.com/abrochard/kubel/issues/106 - (replace-regexp-in-string - "^\*" "" (aref (tabulated-list-get-entry) 0)))) - -(defun kubel--get-context-namespace () - "Utility function to return the proper context and namespace arguments." - (append - (unless (equal kubel-context "") - (list "--context" kubel-context)) - (unless (equal kubel-namespace "") - (list "-n" kubel-namespace)))) - -(defun kubel--get-selector () - "Utility function to return current label selector." - (unless (equal kubel-selector "") - (list "--selector" kubel-selector))) - -(defun kubel--get-command-prefix () - "Utility function to prefix the kubectl command with proper context and namespace." - (mapconcat 'identity (append (list kubel-kubectl) (kubel--get-context-namespace) (kubel--get-selector)) " ")) - -(defun kubel--get-containers (pod-name &optional type) - "List the containers in a pod. - -POD-NAME is the name of the pod. -TYPE is containers or initContainers." - (unless type (setq type "containers")) - (split-string - (kubel--exec-to-string - (format "%s get pod %s -o jsonpath='{.spec.%s[*].name}'" (kubel--get-command-prefix) pod-name type)) " ")) - -(defun kubel--get-pod-labels () - "List labels of pods in a current namespace." - (let* ((raw-labels - (split-string - (replace-regexp-in-string - (regexp-quote ":") "=" - (replace-regexp-in-string - "map\\[\\(.+?\\)\\]" "\\1" - (kubel--exec-to-string - (format "%s get pod -o jsonpath='{.items[*].metadata.labels}'" (kubel--get-command-prefix))))))) - (splitted (mapcan (lambda (s) (split-string s ",")) - raw-labels)) - (cleaned (mapcar (lambda (s) (replace-regexp-in-string "[{|\"|}]" "" s)) splitted)) - (unique (-distinct cleaned))) - unique)) - -(defun kubel--select-resource (name) - "Prompt user to select an instance out of a list of resources. - -NAME is the string name of the resource." - (let ((cmd (format "%s get %s -o=jsonpath='{.items[*].metadata.name}'" - (kubel--get-command-prefix) name))) - (completing-read (concat (s-upper-camel-case name) ": ") - (split-string (kubel--exec-to-string cmd) " ")))) - -(defun kubel--describe-resource (name &optional describe) - "Describe a specific resource. - -NAME is the string name of the resource to decribe. -DESCRIBE is boolean to describe instead of get resource details" - (let* ((resource (kubel--select-resource name)) - (process-name (format "kubel - %s - %s" name resource)) - (callback (lambda () - (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)) - (when (string-equal kubel-output "yaml") - (kubel-yaml-editing-mode)))) - -(defun kubel--show-rollout-revision (type name) - "Show a specific revision of a certain resource. - -TYPE is the resource type. -NAME is the resource name." - (let* ((typename (format "%s/%s" type name)) - (revision (car (split-string (kubel--select-rollout typename)))) - (process-name (format "kubel - rollout - %s - %s" typename revision)) - (callback (goto-char (point-min)))) - (kubel--exec process-name - (list "rollout" "history" typename (format "--revision=%s" revision)) nil callback))) - -(defun kubel--list-rollout (typename) - "Return a list of revisions with format '%number %cause'. - -TYPENAME is the resource type/name." - (let ((cmd (format "%s rollout history %s" (kubel--get-command-prefix) typename))) - (nthcdr 2 (split-string (kubel--exec-to-string cmd) "\n" t)))) - -(defun kubel--select-rollout (typename) - "Select a rollout version. - -TYPENAME is the resource type/name." - (let ((prompt (format "Select a rollout of %s: " typename)) - (rollouts (kubel--list-rollout typename))) - (completing-read prompt rollouts))) - -(defun kubel--is-pod-view () - "Return non-nil if this is the pod view." - (equal (capitalize kubel-resource) "Pods")) - -(defun kubel--is-deployment-view () - "Return non-nil if this is a deployment view." - (-contains? '("Deployments" "deployments" "deployments.apps") kubel-resource)) - -(defun kubel--is-scalable () - "Return non-nil if the resource can be scaled." - (or - (kubel--is-deployment-view) - (-contains? '("ReplicaSets" "replicasets" "replicasets.apps") kubel-resource) - (-contains? '("StatefulSets" "statefulsets" "statefulsets.apps") kubel-resource))) - -(defun kubel-kill-buffer () - "Kill the current buffer." - (interactive) - (when (or (not (buffer-modified-p)) - (not kubel-kill-buffer-query) - (yes-or-no-p "Resource modified; kill anyway? ")) - (kill-buffer (current-buffer)))) - -(defvar kubel-yaml-editing-mode-map - (let ((map (make-sparse-keymap))) - (define-key map (kbd "C-c C-c") #'kubel-apply) - (define-key map (kbd "C-c C-k") #'kubel-kill-buffer) - map) - "Keymap used in `kubel-yaml-editing-mode' buffers.") - -;; interactive -;;;###autoload -(define-derived-mode kubel-yaml-editing-mode yaml-mode "kubel/e" - "Kubel Yaml editing mode. - -Allows simple apply of the changes made. - -\\{kubel-yaml-editing-mode-map}") - -(defun kubel-apply () - "Save the current buffer to a temp file and try to kubectl apply it." - (interactive) - (setq dir-prefix (or - (when (tramp-tramp-file-p default-directory) - (with-parsed-tramp-file-name default-directory nil - (format "/%s%s:%s:" (or hop "") method (if user (concat user "@" host) host)))) - "")) - (let* ((filename-without-tramp-prefix (format "/tmp/kubel/%s-%s.yaml" - (replace-regexp-in-string "/" "_" - (replace-regexp-in-string "\*\\| " "" (buffer-name))) - (floor (float-time)))) - (filename (format "%s%s" dir-prefix filename-without-tramp-prefix))) - (when (y-or-n-p "Apply the changes? ") - (unless (file-exists-p (format "%s/tmp/kubel" dir-prefix)) - (make-directory (format "%s/tmp/kubel" dir-prefix) t)) - (write-region (point-min) (point-max) filename) - (kubel--exec (format "kubectl - apply - %s" filename) (list "apply" "-f" filename-without-tramp-prefix) nil (lambda () (message "Applied %s" filename)))))) - -(defun kubel-get-resource-details (&optional describe) - "Get the details of the resource under the cursor. - - DESCRIBE is the optional param to describe instead of get." - (interactive "P") - (let* ((resource (kubel--get-resource-under-cursor)) - (ctx kubel-context) - (ns kubel-namespace) - (res kubel-resource) - (process-name (format "kubel - %s - %s" kubel-resource resource)) - (callback (lambda () - (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)))) - -(defun kubel--default-tail-arg (args) - "Ugly function to make sure that there is at least the default tail. - -ARGS is the arg list from transient." - (if (car (remove nil (mapcar (lambda (x) - (string-prefix-p "--tail=" x)) args))) - args - (append args (list (concat "--tail=" (format "%s" kubel-log-tail-n)))))) - -(defun kubel-get-pod-logs (&optional args type) - "Get the last N logs of the pod under the cursor. - -ARGS is the arguments list from transient. -TYPE is containers or initContainers." - (interactive - (list (transient-args 'kubel-log-popup))) - (dolist (pod (if (kubel--is-pod-view) - (if (kubel--items-selected-p) - kubel--selected-items - (list (kubel--get-resource-under-cursor))) - (list (kubel--select-resource "Pods")))) - (let* ((type (or type "containers")) - (containers (kubel--get-containers pod type)) - (container (if (equal (length containers) 1) - (car containers) - (completing-read "Select container: " containers))) - (process-name (format "kubel - logs - %s - %s" pod container))) - (kubel--exec process-name - (append '("logs") (kubel--default-tail-arg args) (list pod container)) t nil)))) - -(defun kubel-get-pod-logs--initContainer (&optional args) - "Get the last N logs of the pod under the cursor. - -ARGS is the arguments list from transient." - (interactive - (list (transient-args 'kubel-log-popup))) - (kubel-get-pod-logs args "initContainers")) - -(defun kubel-get-logs-by-labels (&optional args) - "Get the last N logs of the pods by labels. -ARGS is the arguments list from transient." - (interactive - (list (transient-args 'kubel-log-popup))) - (let* ((labels (kubel--get-pod-labels)) - (label (completing-read "Select container: " labels)) - (process-name (format "kubel - logs - %s" label))) - (kubel--exec process-name - (append '("logs") (kubel--default-tail-arg args) '("-l") (list label)) t nil))) - -(defun kubel-copy-resource-name () - "Copy the name of the pod under the cursor." - (interactive) - (kill-new (kubel--get-resource-under-cursor)) - (message "Resource name copied to kill-ring")) - -(defun kubel-copy-log-command () - "Copy the streaming log command of the pod under the cursor." - (interactive) - (kill-new - (format "%s logs -f --tail=%s %s" - (kubel--get-command-prefix) - kubel-log-tail-n - (if (kubel--is-pod-view) - (kubel--get-resource-under-cursor) - (kubel--select-resource "Pods")))) - (message "Log command copied to kill-ring")) - -(defun kubel-copy-command-prefix () - "Copy the kubectl command prefix." - (interactive) - (kill-new (kubel--get-command-prefix)) - (message "Command prefix copied to kill-ring")) - -(defun kubel-copy-last-command () - "Copy the last kubectl command ran." - (interactive) - (kill-new kubel--last-command) - (message (concat "Last command copied: " kubel--last-command))) - -(defun kubel-set-kubectl-config-file (configfile) - "Set the path to the kubectl CONFIGFILE." - (interactive "f") - (let ((configfile (or configfile "~/.kube/config"))) - (if (file-exists-p (expand-file-name configfile)) - (setenv "KUBECONFIG" (expand-file-name configfile)) - (error "Kubectl config file '%s' does not exist!" configfile)))) - -(defun kubel--can-get-namespace () - "Determine if permissions allow for `kubectl get namespace` in current context." - (cond ((eq kubel-use-namespace-list 'on) t) - ((eq kubel-use-namespace-list 'auto) - (progn - (unless kubel--can-get-namespace-cached - (setq kubel--can-get-namespace-cached - (string-match-p "yes\n" - (kubel--exec-to-string - (format "%s --context %s auth can-i list namespaces" kubel-kubectl kubel-context)))))) - kubel--can-get-namespace-cached))) - -(defun kubel--get-namespace () - "Get namespaces for current context, try to recover from cache first." - (unless kubel--namespace-list-cached - (setq kubel--namespace-list-cached - (split-string (kubel--exec-to-string - (format "%s --context %s get namespace -o jsonpath='{.items[*].metadata.name}'" kubel-kubectl kubel-context)) " "))) - kubel--namespace-list-cached) - -(defun kubel--list-namespace () - "List namespace, either from history, or dynamically if possible." - (if (kubel--can-get-namespace) - (kubel--get-namespace) - kubel-namespace-history)) - -(defun kubel--add-namespace-to-history (namespace) - "Add NAMESPACE to history if it isn't there already." - (unless (member namespace kubel-namespace-history) - (push namespace kubel-namespace-history))) - -(defun kubel-set-namespace (&optional refresh) - "Set the namespace. -If called with a prefix argument REFRESH, refreshes -the context caches, including the cached resource list." - (interactive "P") - (when refresh (kubel--invalidate-context-caches)) - (let* ((namespace (completing-read "Namespace: " (kubel--list-namespace) - nil nil nil nil "default")) - (kubel--buffer (get-buffer (kubel--buffer-name))) - (last-default-directory (when kubel--buffer - (with-current-buffer kubel--buffer default-directory)))) - (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)))) - (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." - (unless (member selector kubel-selector-history) - (push selector kubel-selector-history))) - -(defun kubel--get-all-selectors () - "Get all selectors." - (unless kubel--label-values-cached - (let ((labels (kubel--get-pod-labels))) - (setq kubel--label-values-cached labels))) - kubel--label-values-cached) - -(defun kubel--list-selectors () - "List selector expressions from history." - (delete-dups - (append '("none") (kubel--get-all-selectors) - kubel-selector-history))) - -(defun kubel-set-label-selector () - "Set the selector." - (interactive) - (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." - (split-string (kubel--exec-to-string - (format "%s --context %s api-resources -o name --no-headers=true" kubel-kubectl kubel-context)) "\n" t)) - -(defun kubel-set-resource (&optional refresh) - "Set the resource. -If called with a prefix argument REFRESH, refreshes -the context caches, including the cached resource list." - (interactive "P") - (when refresh (kubel--invalidate-context-caches)) - (let* ((current-buffer-name (kubel--buffer-name)) - (resource-list (kubel--kubernetes-resources-list)) - (kubel--buffer (get-buffer current-buffer-name)) - (last-default-directory (when kubel--buffer (with-current-buffer kubel--buffer 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." - (interactive) - (setq kubel-output - (completing-read - "Set output format: " - '("yaml" "json" "wide" "custom-columns=")))) - -(defun kubel-port-forward-pod (p) - "Port forward a pod to your local machine. - -P can be a single number or a localhost:container port pair." - (interactive "sPort: ") - (let* ((port (if (string-match-p ":" p) p (format "%s:%s" p p))) - (pod (if (kubel--is-pod-view) - (kubel--get-resource-under-cursor) - (kubel--select-resource "Pods"))) - (process-name (format "kubel - port-forward - %s:%s" pod port))) - (kubel--exec process-name (list "port-forward" pod port)))) - -(defun kubel-setup-tramp () - "Setup a kubectl TRAMP." - (setq tramp-methods (delete (assoc "kubectl" tramp-methods) tramp-methods)) ;; cleanup previous tramp method - ;; TODO error message if resource is not pod - (add-to-list 'tramp-methods - `("kubectl" - (tramp-login-program ,kubel-kubectl) - (tramp-login-args (,(kubel--get-context-namespace) ("exec" "-it") ("-c" "%u") ("%h") ("--" "sh"))) - (tramp-remote-shell "sh") - (tramp-remote-shell-args ("-i" "-c"))))) ;; add the current context/namespace to tramp methods - -(defun kubel--get-container-under-cursor () - "Get `(container . pod)' name under cursor." - (let* ((pod (if (kubel--is-pod-view) - (kubel--get-resource-under-cursor) - (kubel--select-resource "Pods"))) - (containers (kubel--get-containers pod)) - (container (if (equal (length containers) 1) - (car containers) - (completing-read "Select container: " containers)))) - (cons container pod))) - -(defun kubel--dir-prefix () - "Return the current directory prefix for a TRAMP connection." - (or - (when (tramp-tramp-file-p default-directory) - (with-parsed-tramp-file-name default-directory nil - (format "%s%s:%s|" (or hop "") method (if user (concat user "@" host) host)))) - "")) - -(defun kubel-exec-pod () - "Exec into the pod under the cursor -> `find-file." - (interactive) - (kubel-setup-tramp) - (let* ((dir-prefix (kubel--dir-prefix)) - (con-pod (kubel--get-container-under-cursor))) - (find-file (format "/%skubectl:%s@%s:/" dir-prefix (car con-pod) (cdr con-pod))))) - -(defun kubel--shell-buffer-name (shell-type container pod) - "Generate the name for a pod's shell buffer. - -This uses `kubel-shell-buffer-name-format' as the buffer name -format. See the documentation for it for more information on how -to set this format. - -The values for the current namespace and context are pulled from -the variables `kubel-namespace' and `kubel-context', respectively." - (format-spec kubel-shell-buffer-name-format - `((?t . ,shell-type) - (?c . ,container) - (?p . ,pod) - (?n . ,kubel-namespace) - (?C . ,kubel-context)))) - -(defun kubel-exec-shell-pod () - "Exec into the pod under the cursor -> shell." - (interactive) - (kubel-setup-tramp) - (let* ((dir-prefix (kubel--dir-prefix)) - (con-pod (kubel--get-container-under-cursor)) - (container (car con-pod)) - (pod (cdr con-pod)) - (default-directory (format "/%skubectl:%s@%s:/" dir-prefix container pod))) - (shell (kubel--shell-buffer-name "shell" container pod)))) - -(defun kubel-exec-eshell-pod () - "Exec into the pod under the cursor -> eshell." - (interactive) - (kubel-setup-tramp) - (let* ((dir-prefix (kubel--dir-prefix)) - (con-pod (kubel--get-container-under-cursor)) - (container (car con-pod)) - (pod (cdr con-pod)) - (default-directory (format "/%skubectl:%s@%s:/" dir-prefix container pod)) - (eshell-buffer-name - (kubel--shell-buffer-name "eshell" container pod))) - (eshell))) - -(defun kubel-exec-vterm-pod () - "Exec into the pod under the cursor -> vterm." - (interactive) - (kubel-setup-tramp) - (let* ((dir-prefix (kubel--dir-prefix)) - (con-pod (kubel--get-container-under-cursor)) - (container (car con-pod)) - (pod (cdr con-pod)) - (default-directory (format "/%skubectl:%s@%s:/" dir-prefix container pod)) - (vterm-buffer-name - (kubel--shell-buffer-name "vterm" container pod)) - (vterm-shell "/bin/sh")) - (vterm nil))) - -;;;###autoload -(defun kubel-vterm-setup () - "Adds a vterm enty to the KUBEL-EXEC-POP." - (require 'vterm) - (transient-append-suffix 'kubel-exec-popup "e" - '("v" "Vterm" kubel-exec-vterm-pod))) - -(defun kubel-exec-ansi-term-pod () - "Exec into the pod under the cursor -> `ansi-term'." - (interactive) - (let* ((con-pod (kubel--get-container-under-cursor)) - (container (car con-pod)) - (pod (cdr con-pod)) - (command (format "%s exec %s -c %s -i -t -- /usr/bin/env sh" (kubel--get-command-prefix) pod container))) - (with-current-buffer (ansi-term "bash" (kubel--shell-buffer-name "ansi-term" container pod)) - (process-send-string (current-buffer) (format "%s\n" command))))) - -(defun kubel-exec-eat-pod () - "Exec into the pod under the cursor -> eat." - (interactive) - (unless (fboundp 'eat-other-window) - (user-error "This command requires the `eat' package.")) - (kubel-setup-tramp) - (let* ((dir-prefix (kubel--dir-prefix)) - (con-pod (kubel--get-container-under-cursor)) - (container (car con-pod)) - (pod (cdr con-pod)) - (default-directory (format "/%skubectl:%s@%s:/" dir-prefix container pod)) - (eat-buffer-name (format "*eat:%s" default-directory))) - (eat-other-window))) - -(defun kubel-exec-pod-by-shell-command () - "Prompt shell with kubectl exec command at pod under cursor." - (interactive) - (kubel-setup-tramp) - (let* ((con-pod (kubel--get-container-under-cursor)) - (command (read-string "Shell command: " (format "%s exec %s -c %s -- " (kubel--get-command-prefix) (cdr con-pod) (car con-pod))))) - (shell-command command))) - -(defun kubel-delete-resource () - "Kubectl delete resource under cursor." - (interactive) - (dolist (pod (if (kubel--items-selected-p) - kubel--selected-items - (list (kubel--get-resource-under-cursor)))) - (let* ((process-name (format "kubel - delete %s - %s" kubel-resource pod)) - (args (list "delete" kubel-resource pod))) - (when (transient-args 'kubel-delete-popup) - (setq args (append args (list "--force" "--grace-period=0")))) - (kubel--exec process-name args)))) - -(defun kubel-jab-deployment () - "Make a trivial patch to force a new deployment. - -See https://github.com/kubernetes/kubernetes/issues/27081" - (interactive) - (dolist (deployment (if (kubel--is-deployment-view) - (if (kubel--items-selected-p) - kubel--selected-items - (list (kubel--get-resource-under-cursor))) - (list (kubel--select-resource "Deployments")))) - (let ((process-name (format "kubel - bouncing - %s" deployment))) - (kubel--exec process-name (list "patch" "deployment" deployment "-p" - (format "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"%s\"}}}}}" - (round (time-to-seconds)))))))) - -(defun kubel-scale-replicas (replicas) - "Scale resource replicas. - -REPLICAS is the number of desired replicas." - (interactive (list (read-number "Replicas: "))) - (if (kubel--is-scalable) - (let* ((resource (kubel--get-resource-under-cursor)) - (process-name (format "kubel:scale:%s/%s" kubel-resource resource))) - (kubel--exec 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))) - -(defun kubel-set-filter (filter) - "Set the pod filter. - -FILTER is the filter string." - (interactive "MFilter: ") - (setq kubel-resource-filter filter) - (kubel-refresh)) - -(defun kubel--jump-to-highlight (init search reset) - "Base function to jump to highlight. - -INIT is to be called before searching. -SEARCH is to apply the search and can be repeated safely. -RESET is to be called if the search is nil after the first attempt." - (unless (equal kubel-resource-filter "") - (funcall init) - (unless (funcall search) - (funcall reset) - (funcall search)) - (beginning-of-line))) - -(defun kubel-jump-to-next-highlight () - "Jump to the next hightlighted resrouce." - (interactive) - (kubel--jump-to-highlight - #'end-of-line - (lambda () (re-search-forward kubel-resource-filter (point-max) t)) - #'beginning-of-buffer)) - -(defun kubel-jump-to-previous-highlight () - "Jump to the previou highlighted resrouce." - (interactive) - (kubel--jump-to-highlight - #'beginning-of-line - (lambda () (re-search-backward kubel-resource-filter (point-min) t)) - #'end-of-buffer)) - -(defun kubel-rollout-history () - "See rollout history for resource under cursor." - (interactive) - (kubel--show-rollout-revision kubel-resource (kubel--get-resource-under-cursor))) - -(defun kubel-changelog () - "Opens up the changelog." - (interactive) - (browse-url "https://github.com/abrochard/kubel/blob/master/CHANGELOG.md")) - -(defun kubel-quick-edit () - "Quickly edit any resource." - (interactive) - (kubel--describe-resource - (completing-read "Select resource: " (kubel--kubernetes-resources-list)))) - -(defun kubel-show-process-buffer () - "Show the kubel-process-buffer." - (interactive) - (pop-to-buffer kubel--process-buffer) - (special-mode)) - -(defun kubel-mark-item () - "Mark or unmark the item under cursor." - (interactive) - (let ((item (kubel--get-resource-under-cursor))) - (unless (-contains? kubel--selected-items item) - (progn - (push item kubel--selected-items) - (forward-line 1) - (kubel-refresh))))) - -(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))))) - -(defun kubel-mark-all () - "Mark all items." - (interactive) - (setq kubel--selected-items '()) - (save-excursion - (goto-char (point-min)) - (while (not (eobp)) - (push (kubel--get-resource-under-cursor) kubel--selected-items) - (forward-line 1))) - (kubel-refresh)) - -(defun kubel-unmark-all () - "Unmark all items." - (interactive) - (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 () - "Kubel Exec Menu" - ["Actions" - ("!" "Shell command" kubel-exec-pod-by-shell-command) - ("d" "Dired" kubel-exec-pod) - ("e" "Eshell" kubel-exec-eshell-pod) - ("a" "Ansi-term" kubel-exec-ansi-term-pod) - ("t" "eat" kubel-exec-eat-pod) - ("s" "Shell" kubel-exec-shell-pod)]) - -(transient-define-prefix kubel-log-popup () - "Kubel Log Menu" - ["Arguments" - ("-f" "Follow" "-f") - ("-p" "Previous" "-p") - ("-n" "Tail" "--tail=")] - ["Actions" - ("l" "Tail pod logs" kubel-get-pod-logs) - ("i" "Tail initContainer logs" kubel-get-pod-logs--initContainer) - ("L" "Tail by labels" kubel-get-logs-by-labels)]) - -(transient-define-prefix kubel-copy-popup () - "Kubel Copy Menu" - ["Actions" - ("c" "Copy resource name" kubel-copy-resource-name) - ("l" "Copy pod log command" kubel-copy-log-command) - ("p" "Copy command prefix" kubel-copy-command-prefix) - ("C" "Copy last command" kubel-copy-last-command)]) - -(transient-define-prefix kubel-delete-popup () - "Kubel Delete menu" - ["Arguments" - ("-f" "Force" "--force --grace-period=0")] - ["Actions" - ("k" "Delete resource(s)" kubel-delete-resource)]) - -(transient-define-prefix kubel-describe-popup () - "Kubel Describe Menu" - ["Arguments" - ("-y" "Yaml" "-o yaml")] - ["Actions" - ("RET" "Describe" kubel-get-resource-details)]) - -(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 - ("p" "Port forward" kubel-port-forward-pod) - ("l" "Logs" kubel-log-popup) - ("e" "Exec" kubel-exec-popup) - ("j" "Jab" kubel-jab-deployment) - ("S" "Scale replicas" kubel-scale-replicas)] - ["Settings" - ("C" "Set context" kubel-set-context) - ("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" "Filter" kubel-set-filter) - ("M-n" "Next highlight" kubel-jump-to-next-highlight) - ("M-p" "Previous highlight" kubel-jump-to-previous-highlight) - ("s" "Set label selector" kubel-set-label-selector)] - ["Marking" - ("m" "Mark item" kubel-mark-item) - ("u" "Unmark item" kubel-unmark-item) - ("M" "Mark all items" kubel-mark-all) - ("U" "Unmark all items" kubel-unmark-all)] - ["Utilities" - ("c" "Copy to clipboad..." kubel-copy-popup) - ("$" "Show Process buffer" kubel-show-process-buffer)]]) - -;; mode map -(defvar kubel-mode-map - (let ((map (make-sparse-keymap))) - ;; global - (define-key map (kbd "RET") 'kubel-get-resource-details) - (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-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) - (define-key map (kbd "R") 'kubel-set-resource) - (define-key map (kbd "k") 'kubel-delete-popup) - (define-key map (kbd "f") 'kubel-set-filter) - (define-key map (kbd "r") 'kubel-rollout-history) - (define-key map (kbd "E") 'kubel-quick-edit) - (define-key map (kbd "M-n") 'kubel-jump-to-next-highlight) - (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) - (define-key map (kbd "l") 'kubel-log-popup) - (define-key map (kbd "c") 'kubel-copy-popup) - (define-key map (kbd "e") 'kubel-exec-popup) - (define-key map (kbd "!") 'kubel-exec-pod-by-shell-command) - (define-key map (kbd "j") 'kubel-jab-deployment) - - (define-key map (kbd "m") 'kubel-mark-item) - (define-key map (kbd "u") 'kubel-unmark-item) - (define-key map (kbd "M") 'kubel-mark-all) - (define-key map (kbd "U") 'kubel-unmark-all) - - map) - "Keymap for `kubel-mode'.") - -(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-refresh (&optional directory) - "Refresh the current kubel buffer, calling kubectl using the configured -context, namespace, and resource. - -DIRECTORY is optional for TRAMP support." - (interactive) - (when directory (setq default-directory directory)) - (let ((name (kubel--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)) - (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)))) - (kubel--current-state)) - -;;;###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 (or (get-buffer name) - (get-buffer-create name)))) - (with-current-buffer buf - (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" - "Special mode for kubel buffers." - (buffer-disable-undo) - (kill-all-local-variables) - (setq truncate-lines t) - (setq mode-name "Kubel") - (setq major-mode 'kubel-mode) - (use-local-map kubel-mode-map) - (hl-line-mode 1) - (run-mode-hooks 'kubel-mode-hook)) - -(provide 'kubel) -;;; kubel.el ends here diff --git a/elpa/kubel-20260424.1403/README-elpa b/elpa/kubel-20260424.1403/README-elpa new file mode 100644 index 00000000..a1c1cc10 --- /dev/null +++ b/elpa/kubel-20260424.1403/README-elpa @@ -0,0 +1 @@ +Emacs extension for controlling Kubernetes with limited permissions. diff --git a/elpa/kubel-20260424.1403/kubel-autoloads.el b/elpa/kubel-20260424.1403/kubel-autoloads.el new file mode 100644 index 00000000..0bb89d27 --- /dev/null +++ b/elpa/kubel-20260424.1403/kubel-autoloads.el @@ -0,0 +1,57 @@ +;;; kubel-autoloads.el --- automatically extracted autoloads (do not edit) -*- lexical-binding: t -*- +;; Generated by the `loaddefs-generate' function. + +;; This file is part of GNU Emacs. + +;;; Code: + +(add-to-list 'load-path (or (and load-file-name (directory-file-name (file-name-directory load-file-name))) (car load-path))) + + + +;;; Generated autoloads from kubel.el + +(autoload 'kubel-yaml-editing-mode "kubel" +"Kubel Yaml editing mode. + +Allows simple apply of the changes made. + +\\{kubel-yaml-editing-mode-map} + +In addition to any hooks its parent mode `yaml-mode' might have run, +this mode runs the hook `kubel-yaml-editing-mode-hook', as the final +or penultimate step during initialization." t) +(autoload 'kubel-vterm-setup "kubel" +"Adds a vterm enty to the KUBEL-EXEC-POP.") +(autoload 'kubel-refresh "kubel" +"Refresh the current kubel buffer, calling kubectl using the configured +context, namespace, and resource. + +DIRECTORY is optional for TRAMP support. + +(fn &optional DIRECTORY)" t) +(autoload 'kubel-open "kubel" +"Create a new kubel buffer using passed parameters CONTEXT NAMESPACE RESOURCE. +DIRECTORY is optional for TRAMP support. + +(fn CONTEXT NAMESPACE RESOURCE &optional DIRECTORY)") +(autoload 'kubel "kubel" +"Invoke the kubel buffer. +DIRECTORY is optional for TRAMP support. + +(fn &optional DIRECTORY)" t) +(register-definition-prefixes "kubel" '("kubel-")) + +;;; End of scraped data + +(provide 'kubel-autoloads) + +;; Local Variables: +;; version-control: never +;; no-byte-compile: t +;; no-update-autoloads: t +;; no-native-compile: t +;; coding: utf-8-emacs-unix +;; End: + +;;; kubel-autoloads.el ends here diff --git a/elpa/kubel-20260424.1403/kubel-pkg.el b/elpa/kubel-20260424.1403/kubel-pkg.el new file mode 100644 index 00000000..a909d2df --- /dev/null +++ b/elpa/kubel-20260424.1403/kubel-pkg.el @@ -0,0 +1,12 @@ +;; -*- no-byte-compile: t; lexical-binding: nil -*- +(define-package "kubel" "20260424.1403" + "Control Kubernetes with limited permissions." + '((transient "0.1.0") + (emacs "25.3") + (dash "2.12.0") + (s "1.2.0") + (yaml-mode "0.0.14")) + :url "https://github.com/abrochard/kubel" + :commit "2161e9ae06d2944b5ad313c642948d752f69add0" + :revdesc "2161e9ae06d2" + :keywords '("kubernetes" "k8s" "tools" "processes")) diff --git a/elpa/kubel-20260424.1403/kubel.el b/elpa/kubel-20260424.1403/kubel.el new file mode 100644 index 00000000..5f739110 --- /dev/null +++ b/elpa/kubel-20260424.1403/kubel.el @@ -0,0 +1,1589 @@ +;;; kubel.el --- Control Kubernetes with limited permissions -*- lexical-binding: t; -*- + +;; Copyright (C) 2018, Adrien Brochard + +;; This file is NOT part of Emacs. + +;; This program is free software; you can redistribute it and/or +;; modify it under the terms of the GNU General Public License as +;; published by the Free Software Foundation; either version 2 of the +;; License, or (at your option) any later version. + + + +;; This program is distributed in the hope that it will be useful, but +;; WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +;; General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software +;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +;; USA + +;; Package-Version: 20260424.1403 +;; Package-Revision: 2161e9ae06d2 +;; Author: Adrien Brochard +;; Keywords: kubernetes k8s tools processes +;; URL: https://github.com/abrochard/kubel +;; License: GNU General Public License >= 3 +;; Package-Requires: ((transient "0.1.0") (emacs "25.3") (dash "2.12.0") (s "1.2.0") (yaml-mode "0.0.14")) + +;;; Commentary: + +;; Emacs extension for controlling Kubernetes with limited permissions. + +;;; Usage: + +;; To list the pods in your current context and namespace, call +;; +;; M-x kubel +;; +;; To set the path to the kubectl config file, call: +;; M-x kubel-set-kubectl-config-file +;; +;; or +;; +;; (kubel-set-kubectl-config-file ) +;; ex: (kubel-set-kubectl-config-file "~/.kube/another-config") +;; +;; To set said namespace and context, respectively call +;; +;; M-x kubel-set-namespace +;; M-x kubel-set-context +;; +;; Note that namespace will autocomplete but not context, +;; this is because I interact with kubernetes through a user who +;; does not have permissions to list namespaces. +;; +;; To switch to showing a different resource, use the `R` command or +;; +;; M-x kubel-set-resource +;; +;; This will let you select a resource and re-display the kubel buffer. + +;;; Shortcuts: + +;; On the kubel screen, place your cursor on a resource +;; +;; enter => get resource details +;; C-u enter => describe resource +;; h => help popup +;; ? => help popup +;; E => quick edit any resource +;; g => refresh +;; k => delete popup +;; r => see the rollout history for resource +;; p => port forward pod +;; l => log popup +;; e => exec popup +;; j => jab deployment to force rolling update +;; S => scale replicas +;; C => set context +;; n => set namespace +;; R => set resource +;; K => set kubectl config file +;; F => set output format +;; f => set a substring filter for resource name +;; M-n => jump to the next highlighted resource +;; M-p => jump to previous highlighted resource +;; m => mark item +;; u => unmark item +;; M => mark all items +;; U => unmark all items +;; c => copy popup +;; $ => show process buffer +;; s => show only resources with specified label value + +;;; Customize: + +;; By default, kubel log tails from the last 100 lines, you can change the `kubel-log-tail-n` variable to set another line number. + +;;; Code: + +(require 'transient) +(require 'dash) +(require 's) +(require 'yaml-mode) +(require 'tramp) +(require 'subr-x) +(require 'eshell) +(require 'dired) +(require 'json) + +(defgroup kubel nil "Customisation group for kubel." + :group 'extensions) + +(defconst kubel--list-sort-key + '("NAME" . nil) + "Sort table on this key.") + +(defface kubel-status-running + '((default . (:inherit success))) + "The face to use for the Running status.") + +(defface kubel-status-healthy + '((default . (:inherit success))) + "The face to use for the Healthy status.") + +(defface kubel-status-active + '((default . (:inherit success))) + "The face to use for the Active status.") + +(defface kubel-status-ready + '((default . (:inherit success))) + "The face to use for the Ready status.") + +(defface kubel-status-true + '((default . (:inherit success))) + "The face to use for the True status.") + +(defface kubel-status-unknown + '((default . (:inherit warning))) + "The face to use for the Unknown status.") + +(defface kubel-status-error + '((default . (:inherit error))) + "The face to use for the Error status.") + +(defface kubel-status-evicted + '((default . (:inherit error))) + "The face to use for the Evicted status.") + +(defface kubel-status-memory-pressure + '((default . (:inherit error))) + "The face to use for the Memory Pressure status.") + +(defface kubel-status-pid-pressure + '((default . (:inherit error))) + "The face to use for the PID Pressure status.") + +(defface kubel-status-disk-pressure + '((default . (:inherit error))) + "The face to use for the Disk Pressure status.") + +(defface kubel-status-revision-missing + '((default . (:inherit error))) + "The face to use for the Revision Missing status.") + +(defface kubel-status-revision-failed + '((default . (:inherit error))) + "The face to use for the Revision Failed status.") + +(defface kubel-status-network-unavailable + '((default . (:inherit error))) + "The face to use for the Network Unavailable status.") + +(defface kubel-status-completed + '((default . (:foreground "yellow"))) + "The face to use for the Completed status.") + +(defface kubel-status-crash-loop-backoff + '((default . (:inherit error))) + "The face to use for the Crash Loop Backoff status.") + +(defface kubel-status-terminating + '((default . (:foreground "blue"))) + "The face to use for the Terminating status.") + +(defcustom kubel-status-faces + '(("Running" . kubel-status-running) + ("Healthy" . kubel-status-healthy) + ("Active" . kubel-status-active) + ("Ready" . kubel-status-ready) + ("True" . kubel-status-true) + ("Unknown" . kubel-status-unknown) + ("Error" . kubel-status-error) + ("Evicted" . kubel-status-evicted) + ("MemoryPressure" . kubel-status-memory-pressure) + ("PIDPressure" . kubel-status-pid-pressure) + ("DiskPressure" . kubel-status-disk-pressure) + ("RevisionMissing" . kubel-status-revision-missing) + ("RevisionFailed" . kubel-status-revision-failed) + ("NetworkUnavailable" . kubel-status-network-unavailable) + ("Completed" . kubel-status-completed) + ("CrashLoopBackOff" . kubel-status-crash-loop-backoff) + ("Terminating" . kubel-status-terminating)) + "Associative list of status to face." + :type '(alist :key-type string + :value-type face) + :group 'kubel) + +(defcustom kubel-kubectl "kubectl" + "Kubectl binary path." + :type '(file :must-match t) + :group 'kubel) + +(defconst kubel--process-buffer "*kubel-process*" + "Kubel process buffer name.") + +(defcustom kubel-output "yaml" + "Format for output: json|yaml|wide|custom-columns=..." + :type 'string + :group 'kubel) + +(defcustom kubel-log-tail-n 100 + "Default number of lines to tail." + :type 'integer + :group 'kubel) + +(defcustom kubel-list-wide nil + "Control whether list views show additional colums. + +true - use '-o wide' for list views to show additional columns +false - do not use '-o wide' for list views, hiding additional columns" + :type 'boolean + :group 'kubel) + + +(defcustom kubel-use-namespace-list 'auto + "Control behavior for namespace completion. + +auto - default, use `kubectl auth can-i list namespace` to determine if we can + list namespaces +on - always assume we can list namespaces +off - always assume we cannot list namespaces" + :type '(choice (const :tag "Auto" auto) + (const :tag "On" on) + (const :tag "Off" off)) + :group 'kubel) + +(defcustom kubel-shell-buffer-name-format "kubel:%C:%n:%t:%c@%p" + "Define the name format used for pod shell buffers. + +This is a format string with %-sequences that will be substituted +with information about the shell's connection. The following +%-sequences are defined: + +%t: The shell type. Examples of this are `shell` and `eshell` +%c: The container name +%p: The pod name +%n: The current namespace +%C: The current context" + :type 'string + :group 'kubel) + +(defcustom kubel-kill-buffer-query t + "Non-nil means that killing a modified resource buffer has to be confirmed. +This is used by `kubel-kill-buffer'." + :type 'boolean + :group 'kubel) + +(defcustom kubel-default-namespace "default" + "Default namespace for kubel to use. Change if you have no resources in +`default' namespace." + :type 'string + :group 'kubel) + +(defun kubel--append-to-process-buffer (str) + "Append string STR to the process buffer." + (with-current-buffer (get-buffer-create kubel--process-buffer) + (read-only-mode -1) + (goto-char (point-max)) + (insert (format "%s\n" str)))) + +(defvar-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. + +PROCESS-NAME is the name of the process. +CMD is the kubectl command as a list." + (let ((str-cmd (if (equal 'string (type-of cmd)) cmd (mapconcat #'identity cmd " ")))) + (setq kubel--last-command str-cmd) + (kubel--append-to-process-buffer + (format "[%s]\ncommand: %s" process-name str-cmd)))) + +(defun kubel--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--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." + (kubel--log-command "kubectl-command" cmd) + (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)))) + (unless silence-warnings + (kubel--handle-stderr stderr-output)))) + (kill-buffer stderr-buffer)) + result)) + +(defvar-local kubel-namespace kubel-default-namespace + "Current namespace.") + +(defvar-local kubel-resource "pods" + "Current resource.") + +(defvar-local kubel-context nil + "Current context. Initialized lazily on first use.") + +(defvar-local kubel-resource-filter "" + "Substring filter for resource name.") + +(defvar-local kubel-selector "" + "Label selector for resources.") + +(defvar kubel-namespace-history '() + "List of previously used namespaces.") + +(defvar kubel-selector-history '() + "List of previously used selectors.") + +(defvar-local kubel--can-get-namespace-cached nil) + +(defvar kubel--namespace-list-cached nil) + +(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) + (setq kubel--kubernetes-resources-list-cached + (kubel--fetch-api-resource-list)) + kubel--kubernetes-resources-list-cached)) + +(defun kubel--invalidate-context-caches () + "Invalidate the context caches." + (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)) + +(defun kubel--populate-list () + "Return a list with a tabulated list format and \"tabulated-list-entries\"." + (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 + (list (kubel--get-list-format entrylist) (kubel--get-list-entries entrylist)))) + +(defun kubel--age-to-secs (age) + "Convert AGE in format 1d2h3m4s to seconds." + (let ((rex (rx bol + (opt (group (one-or-more digit)) "d") + (opt (group (one-or-more digit)) "h") + (opt (group (one-or-more digit)) "m") + (opt (group (one-or-more digit)) "s") + eol))) + (if (string-match rex age) + (-sum (--map-indexed + (* (--if-let (match-string (1+ it-index) age) + (string-to-number it) + 0) + it) + '(86400 3600 60 1))) + 0))) + +(defun kubel--make-age-comparator (colnum) + "Return a function that compares two ages at given column COLNUM." + (lambda (row1 row2) + (let ((age1 (elt (cadr row1) colnum)) + (age2 (elt (cadr row2) colnum))) + (< (kubel--age-to-secs age1) + (kubel--age-to-secs age2))))) + +(defun kubel--column-entry (entrylist) + "Return a function of colnum to retrieve an entry in a given column for ENTRYLIST." + (function + (lambda (colnum) + (let* ((name (kubel--column-header entrylist colnum)) + (width (+ 4 (kubel--column-width entrylist colnum))) + (sort (if (member name '("AGE" "DURATION" "LAST SCHEDULE")) + (kubel--make-age-comparator colnum) + t))) + (list name width sort))))) + +(defun kubel--get-list-format (entrylist) + "Get the list format. + +ENTRYLIST is the output of the parsed body." + (defun kubel--get-column-entry (colnum) + (let ((kubel--get-entry (kubel--column-entry entrylist))) + (funcall kubel--get-entry colnum))) + (cl-map 'vector #'kubel--get-column-entry (number-sequence 0 (- (kubel--ncols entrylist) 1)))) + +(defun kubel--update-selected-items (entries) + "Check that all selected items still exist. + +ENTRIES are all resources." + (dolist (i (-difference kubel--selected-items (mapcar #'car entries))) + (setq kubel--selected-items (delete i kubel--selected-items)))) + +(defun kubel--get-list-entries (entrylist) + "Get the entries. + +ENTRYLIST is the output of the parsed body." + (let ((entries (cdr entrylist))) + (kubel--update-selected-items entries) + (mapcar (lambda (x) + (list (car x) + (vconcat [] (mapcar #'kubel--propertize-status x)))) + entries))) + +(defun kubel--parse-body (body) + "Parse the body of kubectl get resource call into a list. + +BODY is the raw output of kubectl get resource." + (let* ((lines (or (nbutlast (split-string body "\n")) '(""))) + (header (car lines)) + ;; Cronjobs have a "LAST SCHEDULE" column, so need to split on 2+ whitespace chars. + (starts (cl-loop for start = 0 then (match-end 0) + while (string-match (rx (>= 2 whitespace)) header start) + collect (match-end 0))) + (position (-zip-with 'cons (cons 0 starts) (append starts '("end")))) + (parse-line (lambda (line) + (mapcar (lambda (pos) + (kubel--extract-value line (car pos) (cdr pos))) + position)))) + (mapcar parse-line lines))) + +(defun kubel--extract-value (line min max) + "Extract value from LINE between MIN and MAX. +If it's just white space, return -, else trim space. +If MAX is the end of the line, dynamically adjust." + (let* ((maxx (if (equal max "end") (length line) max)) + (str (substring-no-properties line min maxx))) + (if (string-match "^ +$" str) + "-" + (string-trim str)))) + +(defun kubel--ncols (entrylist) + "Return the number of columns in ENTRYLIST." + (length (car entrylist))) + +(defun kubel--nrows (entrylist) + "Return the nubmer of rows in ENTRYLIST." + (length entrylist)) + +(defun kubel--column-header (entrylist colnum) + "Return the header for a specific COLNUM in ENTRYLIST." + (nth colnum (car entrylist))) + +(defun kubel--column-width (entrylist colnum) + "Return the width of a specific COLNUM in ENTRYLIST." + (seq-max (mapcar (lambda (x) (length (nth colnum x) )) entrylist))) + +(defun kubel--buffer-name-from-parameters (context namespace resource) + "Return a preconfigured kubel buffer name." + (concat (format "*kubel:%s:%s:%s*" context namespace resource))) + +(defun kubel--buffer-name () + "Return kubel buffer name." + (concat (kubel--buffer-name-from-parameters kubel-context kubel-namespace kubel-resource) + (unless (equal kubel-selector "") + (format " (%s)" kubel-selector)))) + +(defun kubel--items-selected-p () + "Return non-nil if there are items selected." + (>= (length kubel--selected-items) 1)) + +(defun kubel--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)))) + (cond (status-face (propertize status 'face status-face)) + ((not match) (propertize status 'face 'shadow)) + (t status)))) + +(defun kubel--pop-to-buffer (name) + "Utility function to pop to buffer or create it. + +NAME is the buffer name." + (unless (get-buffer name) + (get-buffer-create name)) + (pop-to-buffer-same-window name)) + +(defun kubel--process-error-buffer (process-name) + "Return the error buffer name for the PROCESS-NAME." + (format "*%s:err*" process-name)) + +(defun kubel--sentinel (callback) + "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))) + (kubel--append-to-process-buffer (format "[%s]\nexit-code: %s" process-name exit-status)) + (if (eq 0 exit-status) + (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 list of arguments. +CALLBACK is a function that will be executed when the command completes. +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 "_"))) + (error-buffer (kubel--process-error-buffer process-name)) + (cmd (append (list kubel-kubectl) (kubel--get-context-namespace) args))) + (when (get-buffer buffer-name) + (kill-buffer buffer-name)) + (when (get-buffer error-buffer) + (kill-buffer error-buffer)) + (kubel--log-command process-name cmd) + (make-process :name process-name + :buffer buffer-name + :sentinel (kubel--sentinel callback) + :file-handler t + :stderr (get-buffer-create error-buffer) + :command cmd) + (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. +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)))) + +(defun kubel--get-context-namespace () + "Utility function to return the proper context and namespace arguments." + (append + (unless (or (equal kubel-context nil) (equal kubel-context "")) + (list "--context" kubel-context)) + (unless (equal kubel-namespace "") + (list "-n" kubel-namespace)))) + +(defun kubel--get-selector () + "Utility function to return current label selector." + (unless (equal kubel-selector "") + (list "--selector" kubel-selector))) + +(defun kubel--get-command-prefix () + "Utility function to prefix the kubectl command with proper context and namespace." + (mapconcat 'identity (append (list kubel-kubectl) (kubel--get-context-namespace) (kubel--get-selector)) " ")) + +(defun kubel--get-containers (pod-name &optional type) + "List the containers in a pod. + +POD-NAME is the name of the pod. +TYPE is containers or initContainers." + (unless type (setq type "containers")) + (split-string + (kubel--exec-sync + (format "%s get pod %s -o jsonpath='{.spec.%s[*].name}'" (kubel--get-command-prefix) pod-name type)) " ")) + +(defun kubel--get-pod-labels () + "List labels of pods in a current namespace." + (let* ((raw-labels + (split-string + (replace-regexp-in-string + (regexp-quote ":") "=" + (replace-regexp-in-string + "map\\[\\(.+?\\)\\]" "\\1" + (kubel--exec-sync + (format "%s get pod -o jsonpath='{.items[*].metadata.labels}'" (kubel--get-command-prefix))))))) + (splitted (mapcan (lambda (s) (split-string s ",")) + raw-labels)) + (cleaned (mapcar (lambda (s) (replace-regexp-in-string "[{|\"|}]" "" s)) splitted)) + (unique (-distinct cleaned))) + unique)) + +(defun kubel--select-resource (name) + "Prompt user to select an instance out of a list of resources. + +NAME is the string name of the resource." + (let ((cmd (format "%s get %s -o=jsonpath='{.items[*].metadata.name}'" + (kubel--get-command-prefix) name))) + (completing-read (concat (s-upper-camel-case name) ": ") + (split-string (kubel--exec-sync cmd) " ")))) + +(defun kubel--describe-resource (name &optional describe) + "Describe a specific resource. + +NAME is the string name of the resource to decribe. +DESCRIBE is boolean to describe instead of get resource details" + (let* ((resource (kubel--select-resource name)) + (ctx kubel-context) + (ns kubel-namespace) + (res kubel-resource) + (process-name (format "kubel - %s - %s" name resource)) + (callback (lambda () + (set-buffer-modified-p nil) + (goto-char (point-min))))) + (if describe + (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) + (setq kubel-context ctx) + (setq kubel-namespace ns) + (setq kubel-resource res)))) + +(defun kubel--show-rollout-revision (type name) + "Show a specific revision of a certain resource. + +TYPE is the resource type. +NAME is the resource name." + (let* ((typename (format "%s/%s" type name)) + (revision (car (split-string (kubel--select-rollout typename)))) + (process-name (format "kubel - rollout - %s - %s" typename revision)) + (callback (goto-char (point-min)))) + (kubel--exec-async process-name + (list "rollout" "history" typename (format "--revision=%s" revision)) nil callback))) + +(defun kubel--list-rollout (typename) + "Return a list of revisions with format '%number %cause'. + +TYPENAME is the resource type/name." + (let ((cmd (format "%s rollout history %s" (kubel--get-command-prefix) typename))) + (nthcdr 2 (split-string (kubel--exec-sync cmd) "\n" t)))) + +(defun kubel--select-rollout (typename) + "Select a rollout version. + +TYPENAME is the resource type/name." + (let ((prompt (format "Select a rollout of %s: " typename)) + (rollouts (kubel--list-rollout typename))) + (completing-read prompt rollouts))) + +(defun kubel--is-pod-view () + "Return non-nil if this is the pod view." + (equal (capitalize kubel-resource) "Pods")) + +(defun kubel--is-deployment-view () + "Return non-nil if this is a deployment view." + (-contains? '("Deployments" "deployments" "deployments.apps") kubel-resource)) + +(defun kubel--is-scalable () + "Return non-nil if the resource can be scaled." + (or + (kubel--is-deployment-view) + (-contains? '("ReplicaSets" "replicasets" "replicasets.apps") kubel-resource) + (-contains? '("StatefulSets" "statefulsets" "statefulsets.apps") kubel-resource))) + +(defun kubel-kill-buffer () + "Kill the current buffer." + (interactive) + (when (or (not (buffer-modified-p)) + (not kubel-kill-buffer-query) + (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) + (define-key map (kbd "C-c C-k") #'kubel-kill-buffer) + map) + "Keymap used in `kubel-yaml-editing-mode' buffers.") + +;; interactive +;;;###autoload +(define-derived-mode kubel-yaml-editing-mode yaml-mode "kubel/e" + "Kubel Yaml editing mode. + +Allows simple apply of the changes made. + +\\{kubel-yaml-editing-mode-map}") + +(defun kubel-apply () + "Save the current buffer to a temp file and try to kubectl apply it." + (interactive) + (setq dir-prefix (or + (when (tramp-tramp-file-p default-directory) + (with-parsed-tramp-file-name default-directory nil + (format "/%s%s:%s:" (or hop "") method (if user (concat user "@" host) host)))) + "")) + (let* ((filename-without-tramp-prefix (format "/tmp/kubel/%s-%s.yaml" + (replace-regexp-in-string "/" "_" + (replace-regexp-in-string "\*\\| " "" (buffer-name))) + (floor (float-time)))) + (filename (format "%s%s" dir-prefix filename-without-tramp-prefix))) + (when (y-or-n-p "Apply the changes? ") + (unless (file-exists-p (format "%s/tmp/kubel" dir-prefix)) + (make-directory (format "%s/tmp/kubel" dir-prefix) t)) + (write-region (point-min) (point-max) filename) + (kubel--exec-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. + + 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)) + (callback (lambda () + (setq kubel-context ctx) + (setq kubel-namespace ns) + (setq kubel-resource res) + (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)) 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. + +ARGS is the arg list from transient." + (if (car (remove nil (mapcar (lambda (x) + (string-prefix-p "--tail=" x)) args))) + args + (append args (list (concat "--tail=" (format "%s" kubel-log-tail-n)))))) + +(defun kubel-get-pod-logs (&optional args type) + "Get the last N logs of the pod under the cursor. + +ARGS is the arguments list from transient. +TYPE is containers or initContainers." + (interactive + (list (transient-args 'kubel-log-popup))) + (dolist (pod (if (kubel--is-pod-view) + (if (kubel--items-selected-p) + kubel--selected-items + (list (kubel--get-resource-under-cursor))) + (list (kubel--select-resource "Pods")))) + (let* ((type (or type "containers")) + (containers (kubel--get-containers pod type)) + (container (if (equal (length containers) 1) + (car containers) + (completing-read "Select container: " containers))) + (process-name (format "kubel - logs - %s - %s" pod container))) + (kubel--exec-async process-name + (append '("logs") (kubel--default-tail-arg args) (list pod container)) t nil)))) + +(defun kubel-get-pod-logs--initContainer (&optional args) + "Get the last N logs of the pod under the cursor. + +ARGS is the arguments list from transient." + (interactive + (list (transient-args 'kubel-log-popup))) + (kubel-get-pod-logs args "initContainers")) + +(defun kubel-get-logs-by-labels (&optional args) + "Get the last N logs of the pods by labels. +ARGS is the arguments list from transient." + (interactive + (list (transient-args 'kubel-log-popup))) + (let* ((labels (kubel--get-pod-labels)) + (label (completing-read "Select container: " labels)) + (process-name (format "kubel - logs - %s" label))) + (kubel--exec-async process-name + (append '("logs") (kubel--default-tail-arg args) '("-l") (list label)) t nil))) + +(defun kubel-copy-resource-name () + "Copy the name of the pod under the cursor." + (interactive) + (kill-new (kubel--get-resource-under-cursor)) + (message "Resource name copied to kill-ring")) + +(defun kubel-copy-log-command () + "Copy the streaming log command of the pod under the cursor." + (interactive) + (kill-new + (format "%s logs -f --tail=%s %s" + (kubel--get-command-prefix) + kubel-log-tail-n + (if (kubel--is-pod-view) + (kubel--get-resource-under-cursor) + (kubel--select-resource "Pods")))) + (message "Log command copied to kill-ring")) + +(defun kubel-copy-command-prefix () + "Copy the kubectl command prefix." + (interactive) + (kill-new (kubel--get-command-prefix)) + (message "Command prefix copied to kill-ring")) + +(defun kubel-copy-last-command () + "Copy the last kubectl command ran." + (interactive) + (kill-new kubel--last-command) + (message (concat "Last command copied: " kubel--last-command))) + +(defun kubel-set-kubectl-config-file (configfile) + "Set the path to the kubectl CONFIGFILE." + (interactive "f") + (let ((configfile (or configfile "~/.kube/config"))) + (if (file-exists-p (expand-file-name configfile)) + (setenv "KUBECONFIG" (expand-file-name configfile)) + (error "Kubectl config file '%s' does not exist!" configfile)))) + +(defun kubel--can-get-namespace () + "Determine if permissions allow for `kubectl get namespace` in current context." + (cond ((eq kubel-use-namespace-list 'on) t) + ((eq kubel-use-namespace-list 'auto) + (progn + (unless kubel--can-get-namespace-cached + (setq kubel--can-get-namespace-cached + (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-sync + (format "%s --context %s get namespace -o jsonpath='{.items[*].metadata.name}'" kubel-kubectl kubel-context)) " "))) + kubel--namespace-list-cached) + +(defun kubel--list-namespace () + "List namespace, either from history, or dynamically if possible." + (if (kubel--can-get-namespace) + (kubel--get-namespace) + kubel-namespace-history)) + +(defun kubel--add-namespace-to-history (namespace) + "Add NAMESPACE to history if it isn't there already." + (unless (member namespace kubel-namespace-history) + (push namespace kubel-namespace-history))) + +(defun kubel-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. +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* ((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)))) + +(defun kubel-set-context () + "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-sync (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." + (unless (member selector kubel-selector-history) + (push selector kubel-selector-history))) + +(defun kubel--get-all-selectors () + "Get all selectors." + (unless kubel--label-values-cached + (let ((labels (kubel--get-pod-labels))) + (setq kubel--label-values-cached labels))) + kubel--label-values-cached) + +(defun kubel--list-selectors () + "List selector expressions from history." + (delete-dups + (append '("none") (kubel--get-all-selectors) + kubel-selector-history))) + +(defun kubel-set-label-selector () + "Set the selector." + (interactive) + (let ((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-sync + (format "%s --context %s api-resources -o name --no-headers=true" kubel-kubectl kubel-context)) "\n" t)) + +(defun kubel-set-resource (&optional refresh) + "Set the resource. +If called with a prefix argument REFRESH, refreshes +the context caches, including the cached resource list." + (interactive "P") + (when refresh (kubel--invalidate-context-caches)) + (let* ((current-buffer-name (kubel--buffer-name)) + (resource-list (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)) + (kubel-refresh last-default-directory)))) + +(defun kubel-set-output-format () + "Set output format of kubectl." + (interactive) + (setq kubel-output + (completing-read + "Set output format: " + '("yaml" "json" "wide" "custom-columns=")))) + +(defun kubel-port-forward-pod (p) + "Port forward a pod to your local machine. + +P can be a single number or a localhost:container port pair." + (interactive "sPort: ") + (let* ((port (if (string-match-p ":" p) p (format "%s:%s" p p))) + (pod (if (kubel--is-pod-view) + (kubel--get-resource-under-cursor) + (kubel--select-resource "Pods"))) + (process-name (format "kubel - port-forward - %s:%s" pod port))) + (kubel--exec-async process-name (list "port-forward" pod port)))) + +(defun kubel-setup-tramp () + "Setup a kubectl TRAMP." + (setq tramp-methods (delete (assoc "kubectl" tramp-methods) tramp-methods)) ;; cleanup previous tramp method + ;; TODO error message if resource is not pod + (add-to-list 'tramp-methods + `("kubectl" + (tramp-login-program ,kubel-kubectl) + (tramp-login-args (,(kubel--get-context-namespace) ("exec" "-it") ("-c" "%u") ("%h") ("--" "sh"))) + (tramp-remote-shell "sh") + (tramp-remote-shell-args ("-i" "-c"))))) ;; add the current context/namespace to tramp methods + +(defun kubel--get-container-under-cursor () + "Get `(container . pod)' name under cursor." + (let* ((pod (if (kubel--is-pod-view) + (kubel--get-resource-under-cursor) + (kubel--select-resource "Pods"))) + (containers (kubel--get-containers pod)) + (container (if (equal (length containers) 1) + (car containers) + (completing-read "Select container: " containers)))) + (cons container pod))) + +(defun kubel--dir-prefix () + "Return the current directory prefix for a TRAMP connection." + (or + (when (tramp-tramp-file-p default-directory) + (with-parsed-tramp-file-name default-directory nil + (format "%s%s:%s|" (or hop "") method (if user (concat user "@" host) host)))) + "")) + +(defun kubel-exec-pod () + "Exec into the pod under the cursor -> `find-file." + (interactive) + (kubel-setup-tramp) + (let* ((dir-prefix (kubel--dir-prefix)) + (con-pod (kubel--get-container-under-cursor))) + (find-file (format "/%skubectl:%s@%s:/" dir-prefix (car con-pod) (cdr con-pod))))) + +(defun kubel--shell-buffer-name (shell-type container pod) + "Generate the name for a pod's shell buffer. + +This uses `kubel-shell-buffer-name-format' as the buffer name +format. See the documentation for it for more information on how +to set this format. + +The values for the current namespace and context are pulled from +the variables `kubel-namespace' and `kubel-context', respectively." + (format-spec kubel-shell-buffer-name-format + `((?t . ,shell-type) + (?c . ,container) + (?p . ,pod) + (?n . ,kubel-namespace) + (?C . ,kubel-context)))) + +(defun kubel-exec-shell-pod () + "Exec into the pod under the cursor -> shell." + (interactive) + (kubel-setup-tramp) + (let* ((dir-prefix (kubel--dir-prefix)) + (con-pod (kubel--get-container-under-cursor)) + (container (car con-pod)) + (pod (cdr con-pod)) + (default-directory (format "/%skubectl:%s@%s:/" dir-prefix container pod))) + (shell (kubel--shell-buffer-name "shell" container pod)))) + +(defun kubel-exec-eshell-pod () + "Exec into the pod under the cursor -> eshell." + (interactive) + (kubel-setup-tramp) + (let* ((dir-prefix (kubel--dir-prefix)) + (con-pod (kubel--get-container-under-cursor)) + (container (car con-pod)) + (pod (cdr con-pod)) + (default-directory (format "/%skubectl:%s@%s:/" dir-prefix container pod)) + (eshell-buffer-name + (kubel--shell-buffer-name "eshell" container pod))) + (eshell))) + +(defun kubel-exec-vterm-pod () + "Exec into the pod under the cursor -> vterm." + (interactive) + (kubel-setup-tramp) + (let* ((dir-prefix (kubel--dir-prefix)) + (con-pod (kubel--get-container-under-cursor)) + (container (car con-pod)) + (pod (cdr con-pod)) + (default-directory (format "/%skubectl:%s@%s:/" dir-prefix container pod)) + (vterm-buffer-name + (kubel--shell-buffer-name "vterm" container pod)) + (vterm-shell "/bin/sh")) + (vterm nil))) + +;;;###autoload +(defun kubel-vterm-setup () + "Adds a vterm enty to the KUBEL-EXEC-POP." + (require 'vterm) + (transient-append-suffix 'kubel-exec-popup "e" + '("v" "Vterm" kubel-exec-vterm-pod))) + +(defun kubel-exec-ansi-term-pod () + "Exec into the pod under the cursor -> `ansi-term'." + (interactive) + (let* ((con-pod (kubel--get-container-under-cursor)) + (container (car con-pod)) + (pod (cdr con-pod)) + (command (format "%s exec %s -c %s -i -t -- /usr/bin/env sh" (kubel--get-command-prefix) pod container))) + (with-current-buffer (ansi-term "bash" (kubel--shell-buffer-name "ansi-term" container pod)) + (process-send-string (current-buffer) (format "%s\n" command))))) + +(defun kubel-exec-eat-pod () + "Exec into the pod under the cursor -> eat." + (interactive) + (unless (fboundp 'eat-other-window) + (user-error "This command requires the `eat' package.")) + (kubel-setup-tramp) + (let* ((dir-prefix (kubel--dir-prefix)) + (con-pod (kubel--get-container-under-cursor)) + (container (car con-pod)) + (pod (cdr con-pod)) + (default-directory (format "/%skubectl:%s@%s:/" dir-prefix container pod)) + (eat-buffer-name (format "*eat:%s" default-directory))) + (eat-other-window))) + +(defun kubel-exec-pod-by-shell-command () + "Prompt shell with kubectl exec command at pod under cursor." + (interactive) + (kubel-setup-tramp) + (let* ((con-pod (kubel--get-container-under-cursor)) + (command (read-string "Shell command: " (format "%s exec %s -c %s -- " (kubel--get-command-prefix) (cdr con-pod) (car con-pod))))) + (shell-command command))) + +(defun kubel-delete-resource () + "Kubectl delete resource under cursor." + (interactive) + (dolist (pod (if (kubel--items-selected-p) + kubel--selected-items + (list (kubel--get-resource-under-cursor)))) + (let* ((process-name (format "kubel - delete %s - %s" kubel-resource pod)) + (args (list "delete" kubel-resource pod))) + (when (transient-args 'kubel-delete-popup) + (setq args (append args (list "--force" "--grace-period=0")))) + (kubel--exec-async process-name args)))) + +(defun kubel-jab-deployment () + "Make a trivial patch to force a new deployment. + +See https://github.com/kubernetes/kubernetes/issues/27081" + (interactive) + (dolist (deployment (if (kubel--is-deployment-view) + (if (kubel--items-selected-p) + kubel--selected-items + (list (kubel--get-resource-under-cursor))) + (list (kubel--select-resource "Deployments")))) + (let ((process-name (format "kubel - bouncing - %s" deployment))) + (kubel--exec-async process-name (list "patch" "deployment" deployment "-p" + (format "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"%s\"}}}}}" + (round (time-to-seconds)))))))) + +(defun kubel-scale-replicas (replicas) + "Scale resource replicas. + +REPLICAS is the number of desired replicas." + (interactive (list (read-number "Replicas: "))) + (if (kubel--is-scalable) + (let* ((resource (kubel--get-resource-under-cursor)) + (process-name (format "kubel:scale:%s/%s" kubel-resource resource))) + (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))) + +(defun kubel-set-filter (filter) + "Set the pod filter. + +FILTER is the filter string." + (interactive "MFilter: ") + (setq kubel-resource-filter filter) + (kubel-refresh)) + +(defun kubel--jump-to-highlight (init search reset) + "Base function to jump to highlight. + +INIT is to be called before searching. +SEARCH is to apply the search and can be repeated safely. +RESET is to be called if the search is nil after the first attempt." + (unless (equal kubel-resource-filter "") + (funcall init) + (unless (funcall search) + (funcall reset) + (funcall search)) + (beginning-of-line))) + +(defun kubel-jump-to-next-highlight () + "Jump to the next hightlighted resrouce." + (interactive) + (kubel--jump-to-highlight + #'end-of-line + (lambda () (re-search-forward kubel-resource-filter (point-max) t)) + #'beginning-of-buffer)) + +(defun kubel-jump-to-previous-highlight () + "Jump to the previou highlighted resrouce." + (interactive) + (kubel--jump-to-highlight + #'beginning-of-line + (lambda () (re-search-backward kubel-resource-filter (point-min) t)) + #'end-of-buffer)) + +(defun kubel-rollout-history () + "See rollout history for resource under cursor." + (interactive) + (kubel--show-rollout-revision kubel-resource (kubel--get-resource-under-cursor))) + +(defun kubel-changelog () + "Opens up the changelog." + (interactive) + (browse-url "https://github.com/abrochard/kubel/blob/master/CHANGELOG.md")) + +(defun kubel-quick-edit () + "Quickly edit any resource." + (interactive) + (kubel--describe-resource + (completing-read "Select resource: " (kubel--kubernetes-resources-list)))) + +(defun kubel-show-process-buffer () + "Show the kubel-process-buffer." + (interactive) + (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 the item under cursor and move to next line." + (interactive) + (let ((item (kubel--get-resource-under-cursor)) + (col (current-column))) + (unless (-contains? kubel--selected-items item) + (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 and move to next line." + (interactive) + (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 () + "Mark all items." + (interactive) + (setq kubel--selected-items '()) + (save-excursion + (goto-char (point-min)) + (while (not (eobp)) + (push (kubel--get-resource-under-cursor) kubel--selected-items) + (forward-line 1))) + (tabulated-list-print t)) + +(defun kubel-unmark-all () + "Unmark all items." + (interactive) + (setq kubel--selected-items '()) + (tabulated-list-print t)) + +(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 () + "Kubel Exec Menu" + ["Actions" + ("!" "Shell command" kubel-exec-pod-by-shell-command) + ("d" "Dired" kubel-exec-pod) + ("e" "Eshell" kubel-exec-eshell-pod) + ("a" "Ansi-term" kubel-exec-ansi-term-pod) + ("t" "eat" kubel-exec-eat-pod) + ("s" "Shell" kubel-exec-shell-pod)]) + +(transient-define-prefix kubel-log-popup () + "Kubel Log Menu" + ["Arguments" + ("-f" "Follow" "-f") + ("-p" "Previous" "-p") + ("-n" "Tail" "--tail=")] + ["Actions" + ("l" "Tail pod logs" kubel-get-pod-logs) + ("i" "Tail initContainer logs" kubel-get-pod-logs--initContainer) + ("L" "Tail by labels" kubel-get-logs-by-labels)]) + +(transient-define-prefix kubel-copy-popup () + "Kubel Copy Menu" + ["Actions" + ("c" "Copy resource name" kubel-copy-resource-name) + ("l" "Copy pod log command" kubel-copy-log-command) + ("p" "Copy command prefix" kubel-copy-command-prefix) + ("C" "Copy last command" kubel-copy-last-command)]) + +(transient-define-prefix kubel-delete-popup () + "Kubel Delete menu" + ["Arguments" + ("-f" "Force" "--force --grace-period=0")] + ["Actions" + ("k" "Delete resource(s)" kubel-delete-resource)]) + +(transient-define-prefix kubel-describe-popup () + "Kubel Describe Menu" + ["Arguments" + ("-y" "Yaml" "-o yaml")] + ["Actions" + ("RET" "Describe" kubel-get-resource-details)]) + +(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 + ("p" "Port forward" kubel-port-forward-pod) + ("l" "Logs" kubel-log-popup) + ("e" "Exec" kubel-exec-popup) + ("j" "Jab" kubel-jab-deployment) + ("S" "Scale replicas" kubel-scale-replicas)] + ["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" "Filter" kubel-set-filter) + ("M-n" "Next highlight" kubel-jump-to-next-highlight) + ("M-p" "Previous highlight" kubel-jump-to-previous-highlight) + ("s" "Set label selector" kubel-set-label-selector)] + ["Marking" + ("m" "Mark item" kubel-mark-item) + ("u" "Unmark item" kubel-unmark-item) + ("M" "Mark all items" kubel-mark-all) + ("U" "Unmark all items" kubel-unmark-all)] + ["Utilities" + ("c" "Copy to clipboad..." kubel-copy-popup) + ("$" "Show Process buffer" kubel-show-process-buffer)]]) + +;; mode map +(defvar kubel-mode-map + (let ((map (make-sparse-keymap))) + ;; global + (define-key map (kbd "RET") 'kubel-get-resource-details) + (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) + (define-key map (kbd "F") 'kubel-set-output-format) + (define-key map (kbd "R") 'kubel-set-resource) + (define-key map (kbd "k") 'kubel-delete-popup) + (define-key map (kbd "f") 'kubel-set-filter) + (define-key map (kbd "r") 'kubel-rollout-history) + (define-key map (kbd "E") 'kubel-quick-edit) + (define-key map (kbd "M-n") 'kubel-jump-to-next-highlight) + (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) + (define-key map (kbd "l") 'kubel-log-popup) + (define-key map (kbd "c") 'kubel-copy-popup) + (define-key map (kbd "e") 'kubel-exec-popup) + (define-key map (kbd "!") 'kubel-exec-pod-by-shell-command) + (define-key map (kbd "j") 'kubel-jab-deployment) + + (define-key map (kbd "m") 'kubel-mark-item) + (define-key map (kbd "u") 'kubel-unmark-item) + (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'.") + +(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-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--clear-header-error) + (when directory (setq default-directory directory)) + (let ((name (kubel--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)) + (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)))) + (kubel--current-state)) + +;;;###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))))) + +(defun kubel--init-context () + "Initialize context and namespace if not already set. +Defers the kubectl call until first use to avoid blocking Emacs startup." + (unless kubel-context + (setq kubel-context + (replace-regexp-in-string + "\n" "" (kubel--exec-sync "kubectl config current-context"))) + (setq kubel-namespace kubel-default-namespace))) + +;;;###autoload +(defun kubel (&optional directory) + "Invoke the kubel buffer. +DIRECTORY is optional for TRAMP support." + (interactive) + (let* ((name (kubel--buffer-name)) + (buf (or (get-buffer name) + (get-buffer-create name)))) + (with-current-buffer buf + (switch-to-buffer (current-buffer)) + (unless (eq major-mode 'kubel-mode) + (kubel-mode)) + (kubel--init-context) + (kubel-refresh directory)))) + +(define-derived-mode kubel-mode tabulated-list-mode "Kubel" + "Special mode for kubel buffers." + (buffer-disable-undo) + (kill-all-local-variables) + (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)) + +(provide 'kubel) +;;; kubel.el ends here