]> git.bundesbrandschatzamt.de Git - emacsconfig.git/commitdiff
add kubel including my fix for fetch-api-resource-list
authorAndreas Gerler <baronNOSPAMbundesbrandschatzamt.de>
Wed, 10 Feb 2021 14:01:51 +0000 (15:01 +0100)
committerAndreas Gerler <baronNOSPAMbundesbrandschatzamt.de>
Wed, 10 Feb 2021 14:01:51 +0000 (15:01 +0100)
elpa/kubel-20201223.1730/kubel-autoloads.el [new file with mode: 0644]
elpa/kubel-20201223.1730/kubel-pkg.el [new file with mode: 0644]
elpa/kubel-20201223.1730/kubel.el [new file with mode: 0644]
elpa/kubel-readme.txt [new file with mode: 0644]

diff --git a/elpa/kubel-20201223.1730/kubel-autoloads.el b/elpa/kubel-20201223.1730/kubel-autoloads.el
new file mode 100644 (file)
index 0000000..c04476a
--- /dev/null
@@ -0,0 +1,29 @@
+;;; kubel-autoloads.el --- automatically extracted autoloads
+;;
+;;; Code:
+
+(add-to-list 'load-path (directory-file-name
+                         (or (file-name-directory #$) (car load-path))))
+
+\f
+;;;### (autoloads nil "kubel" "kubel.el" (0 0 0 0))
+;;; Generated autoloads from kubel.el
+
+(autoload 'kubel "kubel" "\
+Invoke the kubel buffer.
+
+DIRECTORY is optional for TRAMP support.
+
+\(fn &optional DIRECTORY)" t nil)
+
+(if (fboundp 'register-definition-prefixes) (register-definition-prefixes "kubel" '("kubel-")))
+
+;;;***
+\f
+;; Local Variables:
+;; version-control: never
+;; no-byte-compile: t
+;; no-update-autoloads: t
+;; coding: utf-8
+;; End:
+;;; kubel-autoloads.el ends here
diff --git a/elpa/kubel-20201223.1730/kubel-pkg.el b/elpa/kubel-20201223.1730/kubel-pkg.el
new file mode 100644 (file)
index 0000000..86000be
--- /dev/null
@@ -0,0 +1,2 @@
+;;; Generated package description from /Users/andreas/.emacs.d/elpa/kubel-20201223.1730/kubel.el  -*- no-byte-compile: t -*-
+(define-package "kubel" "20201223.1730" "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")) :commit "9b1aa4830a4470ff8f11ecc0bc80b5cdaee473fd" :authors '(("Adrien Brochard")) :maintainer '("Adrien Brochard") :keywords '("kubernetes" "k8s" "tools" "processes") :url "https://github.com/abrochard/kubel")
diff --git a/elpa/kubel-20201223.1730/kubel.el b/elpa/kubel-20201223.1730/kubel.el
new file mode 100644 (file)
index 0000000..d1741d8
--- /dev/null
@@ -0,0 +1,1103 @@
+;;; kubel.el --- Control Kubernetes with limited permissions -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2018, Adrien Brochard
+
+;; This file is NOT part of Emacs.
+
+;; This  program is  free  software; you  can  redistribute it  and/or
+;; modify it  under the  terms of  the GNU  General Public  License as
+;; published by the Free Software  Foundation; either version 2 of the
+;; License, or (at your option) any later version.
+
+
+
+;; This program is distributed in the hope that it will be useful, but
+;; WITHOUT  ANY  WARRANTY;  without   even  the  implied  warranty  of
+;; MERCHANTABILITY or FITNESS  FOR A PARTICULAR PURPOSE.   See the GNU
+;; General Public License for more details.
+
+;; You should have  received a copy of the GNU  General Public License
+;; along  with  this program;  if  not,  write  to the  Free  Software
+;; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+;; USA
+
+;; Version: 1.0
+;; Package-Version: 20201223.1730
+;; Package-Commit: 9b1aa4830a4470ff8f11ecc0bc80b5cdaee473fd
+;; Author: Adrien Brochard
+;; Keywords: kubernetes k8s tools processes
+;; URL: https://github.com/abrochard/kubel
+;; License: GNU General Public License >= 3
+;; Package-Requires: ((transient "0.1.0") (emacs "25.3") (dash "2.12.0") (s "1.2.0") (yaml-mode "0.0.14"))
+
+;;; Commentary:
+
+;; Emacs extension for controlling Kubernetes with limited permissions.
+
+;;; Usage:
+
+;; To list the pods in your current context and namespace, call
+;;
+;; M-x kubel
+;;
+;; To set the path to the kubectl config file, call:
+;; M-x kubel-set-kubectl-config-file
+;;
+;; or
+;;
+;; (kubel-set-kubectl-config-file <path to desired config file>)
+;; ex: (kubel-set-kubectl-config-file "~/.kube/another-config")
+;;
+;; To set said namespace and context, respectively call
+;;
+;; M-x kubel-set-namespace
+;; M-x kubel-set-context
+;;
+;; Note that namespace will autocomplete but not context,
+;; this is because I interact with kubernetes through a user who
+;; does not have permissions to list namespaces.
+;;
+;; To switch to showing a different resource, use the `R` command or
+;;
+;; M-x kubel-set-resource
+;;
+;; This will let you select a resource and re-display the kubel buffer.
+
+;;; Shortcuts:
+
+;; On the kubel screen, place your cursor on a resource
+;;
+;; enter => get resource details
+;; 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
+;; 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
+
+;;; Customize:
+
+;; By default, kubel log tails from the last 100 lines, you can change the `kubel-log-tail-n` variable to set another line number.
+
+;;; Code:
+
+(require 'transient)
+(require 'dash)
+(require 's)
+(require 'yaml-mode)
+(require 'tramp)
+(require 'subr-x)
+(require 'eshell)
+(require 'dired)
+
+(defgroup kubel nil "Customisation group for kubel."
+  :group 'extensions)
+
+(defvar kubel--list-format
+  [("Name" 50 t)
+   ("Ready" 10 t)
+   ("Status" 20 t)
+   ("Restarts" 10 t)
+   ("Age" 15 t)]
+  "List format.")
+
+(defconst kubel--list-sort-key
+  '("NAME" . nil)
+  "Sort table on this key.")
+
+(defconst kubel--status-colors
+  '(("Running" . "green")
+    ("Healthy" . "green")
+    ("Active" . "green")
+    ("Ready" . "green")
+    ("True" . "green")
+    ("Unknown" . "orange")
+    ("Error" . "red")
+    ("Evicted" . "red")
+    ("MemoryPressure" . "red")
+    ("PIDPressure" . "red")
+    ("DiskPressure" . "red")
+    ("RevisionMissing" . "red")
+    ("RevisionFailed" . "red")
+    ("NetworkUnavailable" . "red")
+    ("Completed" . "yellow")
+    ("CrashLoopBackOff" . "red")
+    ("Terminating" . "blue"))
+  "Associative list of status to color.")
+
+(defconst kubel--process-buffer "*kubel-process*"
+  "Kubel process buffer name.")
+
+(defcustom kubel-output "yaml"
+  "Format for output: json|yaml|wide|custom-columns=..."
+  :type 'string
+  :group 'kubel)
+
+(defcustom kubel-log-tail-n 100
+  "Default number of lines to tail."
+  :type 'integer
+  :group 'kubel)
+
+(defcustom kubel-use-namespace-list 'auto
+  "Control behavior for namespace completion.
+
+auto - default, use `kubectl auth can-i list namespace` to determine if we can list namespaces
+on - always assume we can list namespaces
+off - always assume we cannot list namespaces"
+  :type 'symbol
+  :group 'kubel
+  :options '('auto 'on 'off))
+
+(defun kubel--append-to-process-buffer (str)
+  "Append string STR to the process buffer."
+  (with-current-buffer (get-buffer-create kubel--process-buffer)
+    (read-only-mode -1)
+    (goto-char (point-max))
+    (insert (format "%s\n" str))))
+
+(defvar kubel--last-command nil)
+
+(defun kubel--log-command (process-name cmd)
+  "Log the kubectl command to the process buffer.
+
+PROCESS-NAME is the name of the process.
+CMD is the kubectl command as a list."
+  (let ((str-cmd (if (equal 'string (type-of cmd)) cmd (mapconcat #'identity cmd " "))))
+    (setq kubel--last-command str-cmd)
+    (kubel--append-to-process-buffer
+     (format "[%s]\ncommand: %s" process-name str-cmd))))
+
+(defun kubel--exec-to-string (cmd)
+  "Replace \"shell-command-to-string\" to log to process buffer.
+
+CMD is the command string to run."
+  (kubel--log-command "kubectl-command" cmd)
+  (shell-command-to-string cmd))
+
+(defvar kubel-namespace "default"
+  "Current namespace.")
+
+(defvar kubel-resource "Pods"
+  "Current resource.")
+
+(defvar kubel-context
+  (replace-regexp-in-string
+   "\n" "" (kubel--exec-to-string "kubectl config current-context"))
+  "Current context.  Tries to smart default.")
+
+(defvar kubel-resource-filter ""
+  "Substring filter for resource name.")
+
+(defvar kubel--line-number nil
+  "Store the current line number to jump back after a refresh.")
+
+(defvar kubel-namespace-history '()
+  "List of previously used namespaces.")
+
+;; fallback list of resources if the version of kubectl doesn't support api-resources command
+(defvar kubel-kubernetes-resources-list
+  '("Pods"
+       "Services"
+       "Namespaces"
+       "Nodes"
+       "Configmaps"
+       "Secrets"
+       "Bindings"
+       "PersistentVolumeClaims"
+       "PersistentVolumes"
+       "ReplicationControllers"
+       "ResourceQuotas"
+       "ServiceAccounts"
+       "Deployments"
+       "DaemonSets"
+       "ReplicaSets"
+       "StatefulSets"
+       "Jobs"
+    "hpa"
+       "Images"
+       "Ingresses"
+       "ClusterRoles"
+       "RoleBindings"
+       "Roles"))
+
+(defvar kubel--kubernetes-version-cached nil)
+
+(defvar kubel--kubernetes-resources-list-cached nil)
+
+(defvar kubel--can-get-namespace-cached nil)
+
+(defvar kubel--namespace-list-cached nil)
+
+(defvar kubel--selected-items '())
+
+(defun kubel--invalidate-context-caches ()
+  "Invalidate the context caches."
+  (setq kubel--kubernetes-resources-list-cached nil)
+  (setq kubel--kubernetes-version-cached nil)
+  (setq kubel--can-get-namespace-cached nil)
+  (setq kubel--namespace-list-cached nil))
+
+(defun kubel-kubernetes-version ()
+  "Return a list with (major-version minor-version patch)."
+  (let ((version-string (if (null kubel--kubernetes-version-cached)
+                            (setq kubel--kubernetes-version-cached
+                                  (kubel--exec-to-string "kubectl version"))
+                          kubel--kubernetes-version-cached)))
+    (string-match "GitVersion:\"v\\([0-9]*\\)\.\\([0-9]*\\)\.\\([0-9]*\\)[^0-9].*\"" version-string)
+    (list
+     (string-to-number (match-string 1 version-string))
+     (string-to-number (match-string 2 version-string))
+     (string-to-number (match-string 3 version-string)))))
+
+(defun kubel-kubernetes-compatible-p (version)
+  "Return TRUE if kubernetes version is greater than or equal to VERSION.
+VERSION should be a list of (major-version minor-version patch)."
+  (let*
+      ((kubernetes-version (kubel-kubernetes-version))
+       (kubernetes-major-version (nth 0 kubernetes-version))
+       (kubernetes-minor-version (nth 1 kubernetes-version))
+       (kubernetes-patch-version (nth 2 kubernetes-version)))
+    (and
+     (<= (nth 0 version) kubernetes-major-version)
+     (or (<= (nth 1 version) kubernetes-minor-version) (< (nth 0 version) kubernetes-major-version))
+     (or (<= (nth 2 version) kubernetes-patch-version) (< (nth 1 version) kubernetes-minor-version)))))
+
+(defun kubel--populate-list ()
+  "Return a list with a tabulated list format and \"tabulated-list-entries\"."
+  (let*  ((body (kubel--exec-to-string (concat (kubel--get-command-prefix) " get " kubel-resource)))
+             (entrylist (kubel--parse-body body)))
+    (when (string-prefix-p "No resources found" body)
+         (message "No resources found"))  ;; TODO exception here
+    (list (kubel--get-list-format entrylist) (kubel--get-list-entries entrylist))))
+
+(defun kubel--column-entry (entrylist)
+  "Return a function of colnum to retrieve an entry in a given column for ENTRYLIST."
+  (function
+   (lambda (colnum)
+     (list (kubel--column-header entrylist colnum) (+ 4 (kubel--column-width entrylist colnum)) t))))
+
+
+(defun kubel--get-list-format (entrylist)
+  "Get the list format.
+
+ENTRYLIST is the output of the parsed body."
+  (defun kubel--get-column-entry (colnum)
+    (let ((kubel--get-entry (kubel--column-entry entrylist)))
+         (funcall kubel--get-entry colnum)))
+  (cl-map 'vector #'kubel--get-column-entry (number-sequence 0 (- (kubel--ncols entrylist) 1))))
+
+(defun kubel--update-selected-items (entries)
+  "Check that all selected items still exist.
+
+ENTRIES are all resources."
+  (dolist (i (-difference kubel--selected-items (mapcar #'car entries)))
+    (setq kubel--selected-items (delete i kubel--selected-items))))
+
+(defun kubel--get-list-entries (entrylist)
+  "Get the entries.
+
+ENTRYLIST is the output of the parsed body."
+  (let ((entries (cdr entrylist)))
+    (kubel--update-selected-items entries)
+    (mapcar (lambda (x)
+              (list (car x)
+                    (vconcat [] (mapcar #'kubel--propertize-status x))))
+            entries)))
+
+(defun kubel--parse-body (body)
+  "Parse the body of kubectl get resource call into a list.
+
+BODY is the raw output of kubectl get resource."
+  (let* ((lines (nbutlast (split-string body "\n")))
+         (header (car lines))
+         (cols (split-string header))
+         (start-pos (mapcar (lambda (x) (string-match x header)) cols))
+         (end-pos (delete 0 (append start-pos '("end"))))
+         (position (-zip-with 'cons start-pos end-pos))
+         (parse-line (lambda (line)
+                       (mapcar (lambda (pos)
+                                 (kubel--extract-value line (car pos) (cdr pos)))
+                               position))))
+    (mapcar parse-line lines)))
+
+(defun kubel--extract-value (line min max)
+  "Extract value from LINE between MIN and MAX.
+If it's just white space, return -, else trim space.
+If MAX is the end of the line, dynamically adjust."
+  (let* ((maxx (if (equal max "end") (length line) max))
+         (str (substring-no-properties line min maxx)))
+    (if (string-match "^ +$" str)
+        "-"
+      (string-trim str))))
+
+(defun kubel--ncols (entrylist)
+  "Return the number of columns in ENTRYLIST."
+  (length (car entrylist)))
+
+(defun kubel--nrows (entrylist)
+  "Return the nubmer of rows in ENTRYLIST."
+  (length entrylist))
+
+(defun kubel--column-header (entrylist colnum)
+  "Return the header for a specific COLNUM in ENTRYLIST."
+  (nth colnum (car entrylist)))
+
+(defun kubel--column-width (entrylist colnum)
+  "Return the width of a specific COLNUM in ENTRYLIST."
+  (seq-max (mapcar (lambda (x) (length (nth colnum x) )) entrylist)))
+
+(defun kubel--buffer-name ()
+  "Return kubel buffer name."
+  (format "*kubel (%s) [%s]: %s*" kubel-namespace kubel-context kubel-resource))
+
+(defun kubel--items-selected-p ()
+  "Return non-nil if there are items selected."
+  (>= (length kubel--selected-items) 1))
+
+(defun kubel--propertize-status (status)
+  "Return the status in proper font color.
+
+STATUS is the pod status string."
+  (let ((pair (cdr (assoc status kubel--status-colors)))
+        (match (or (equal kubel-resource-filter "") (string-match-p kubel-resource-filter status)))
+        (selected (and (kubel--items-selected-p) (-contains? kubel--selected-items status))))
+    (cond (pair (propertize status 'font-lock-face `(:foreground ,pair)))
+          (selected (propertize (concat "*" status) 'face 'dired-marked))
+          ((not match) (propertize status 'font-lock-face '(:foreground "darkgrey")))
+          (t status))))
+
+(defun kubel--pop-to-buffer (name)
+  "Utility function to pop to buffer or create it.
+
+NAME is the buffer name."
+  (unless (get-buffer name)
+    (get-buffer-create name))
+  (pop-to-buffer-same-window name))
+
+(defun kubel--process-error-buffer (process-name)
+  "Return the error buffer name for the PROCESS-NAME."
+  (format "*%s:err*" process-name))
+
+(defun kubel--sentinel (process _)
+  "Sentinel function for PROCESS."
+  (let ((process-name (process-name process))
+        (exit-status (process-exit-status process)))
+    (kubel--append-to-process-buffer (format "[%s]\nexit-code: %s" process-name exit-status))
+    (unless (eq 0 exit-status)
+       (let ((err (with-current-buffer (kubel--process-error-buffer process-name)
+                 (buffer-string))))
+      (kubel--append-to-process-buffer (format "error: %s" err))
+      (error (format "Kubel process %s error: %s" process-name err))))))
+
+(defun kubel--exec (process-name args &optional readonly)
+  "Utility function to run commands in the proper context and namespace.
+
+PROCESS-NAME is an identifier for the process.  Default to \"kubel-command\".
+ARGS is a ist of arguments.
+READONLY If true buffer will be in readonly mode(view-mode)."
+  (when (equal process-name "")
+    (setq process-name "kubel-command"))
+  (let ((buffer-name (format "*%s*" process-name))
+        (error-buffer (kubel--process-error-buffer process-name))
+        (cmd (append (list "kubectl") (kubel--get-context-namespace) args)))
+    (when (get-buffer buffer-name)
+      (kill-buffer buffer-name))
+    (when (get-buffer error-buffer)
+      (kill-buffer error-buffer))
+    (kubel--log-command process-name cmd)
+    (make-process :name process-name
+                  :buffer buffer-name
+                  :sentinel #'kubel--sentinel
+                  :file-handler t
+                  :stderr error-buffer
+                  :command cmd)
+    (pop-to-buffer buffer-name)
+    (if readonly
+        (with-current-buffer buffer-name
+          (view-mode)))))
+
+(defun kubel--get-resource-under-cursor ()
+  "Utility function to get the name of the resource under the cursor.
+Strip the `*` prefix if the resource is selected"
+  (replace-regexp-in-string
+   "^\*" "" (aref (tabulated-list-get-entry) 0)))
+
+(defun kubel--get-context-namespace ()
+  "Utility function to return the proper context and namespace arguments."
+  (append
+   (unless (equal kubel-context "")
+     (list "--context" kubel-context))
+   (unless (equal kubel-namespace "default")
+     (list "-n" kubel-namespace))))
+
+(defun kubel--get-command-prefix ()
+  "Utility function to prefix the kubectl command with proper context and namespace."
+  (mapconcat 'identity (append '("kubectl") (kubel--get-context-namespace)) " "))
+
+(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."
+  (delete-dups
+   (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))))))))
+
+(defun kubel--select-resource (name)
+  "Prompt user to select an instance out of a list of resources.
+
+NAME is the string name of the resource."
+  (let ((cmd (format "%s get %s -o=jsonpath='{.items[*].metadata.name}'"
+                     (kubel--get-command-prefix) name)))
+    (completing-read (concat (s-upper-camel-case name) ": ")
+                     (split-string (kubel--exec-to-string cmd) " "))))
+
+(defun kubel--describe-resource (name &optional describe)
+  "Describe a specific resource.
+
+NAME is the string name of the resource to decribe.
+DESCRIBE is boolean to describe instead of get resource details"
+  (let* ((resource (kubel--select-resource name))
+         (process-name (format "kubel - %s - %s" name resource)))
+    (if describe
+           (kubel--exec process-name (list "describe" name resource))
+      (kubel--exec process-name (list "get" name "-o" kubel-output resource)))
+    (when (string-equal kubel-output "yaml")
+      (yaml-mode)
+      (kubel-yaml-editing-mode))
+    (goto-char (point-min))))
+
+(defun kubel--show-rollout-revision (type name)
+  "Show a specific revision of a certain resource.
+
+TYPE is the resource type.
+NAME is the resource name."
+  (let* ((typename (format "%s/%s" type name))
+         (revision (car (split-string (kubel--select-rollout typename))))
+         (process-name (format "kubel - rollout - %s - %s" typename revision)))
+    (kubel--exec process-name
+                 (list "rollout" "history" typename (format "--revision=%s" revision)))
+    (goto-char (point-min))))
+
+(defun kubel--list-rollout (typename)
+  "Return a list of revisions with format '%number   %cause'.
+
+TYPENAME is the resource type/name."
+  (let ((cmd (format "%s rollout history %s" (kubel--get-command-prefix) typename)))
+    (nthcdr 2 (split-string (kubel--exec-to-string cmd) "\n"))))
+
+(defun kubel--select-rollout (typename)
+  "Select a rollout version.
+
+TYPENAME is the resource type/name."
+  (let ((prompt (format "Select a rollout of %s: " typename))
+        (rollouts (kubel--list-rollout typename)))
+    (completing-read prompt rollouts)))
+
+(defun kubel--is-pod-view ()
+  "Return non-nil if this is the pod view."
+  (equal (capitalize kubel-resource) "Pods"))
+
+(defun kubel--is-deployment-view ()
+  "Return non-nil if this is the pod view."
+  (-contains? '("Deployments" "deployments" "deployments.apps") kubel-resource))
+
+(defun kubel--save-line ()
+  "Save the current line number if the view is unchanged."
+  (if (equal (buffer-name (current-buffer))
+             (kubel--buffer-name))
+      (setq kubel--line-number (+ 1 (count-lines 1 (point))))
+    (setq kubel--line-number nil)))
+
+(defun kubel--jump-back-to-line ()
+  "Jump back to the last cached line number."
+  (when kubel--line-number
+    (goto-line kubel--line-number)))
+
+;; interactive
+(define-minor-mode kubel-yaml-editing-mode
+  "Kubel Yaml editing mode.
+Use C-c C-c to kubectl apply the current yaml buffer."
+  :init-value nil
+  :keymap (let ((map (make-sparse-keymap)))
+            (define-key map (kbd "C-c C-c") 'kubel-apply)
+            map))
+
+(defun kubel-apply ()
+  "Save the current buffer to a temp file and try to kubectl apply it."
+  (interactive)
+  (setq dir-prefix (or
+                           (when (tramp-tramp-file-p default-directory)
+                             (with-parsed-tramp-file-name default-directory nil
+                                   (format "/%s%s:%s@%s:" (or hop "") method user host)))
+                           ""))
+
+  (let* ((filename-without-tramp-prefix (format "/tmp/kubel/%s-%s.yaml"
+                                                                       (replace-regexp-in-string "\*\\| " "" (buffer-name))
+                                                                       (floor (float-time))))
+            (filename (format "%s%s" dir-prefix filename-without-tramp-prefix)))
+    (when (y-or-n-p "Apply the changes? ")
+      (unless  (file-exists-p (format "%s/tmp/kubel" dir-prefix))
+           (make-directory (format "%s/tmp/kubel" dir-prefix) t))
+      (write-region (point-min) (point-max) filename)
+      (kubel--exec (format "kubectl - apply - %s" filename) (list "apply" "-f" filename-without-tramp-prefix))
+      (message "Applied %s" filename))))
+
+(defun kubel-get-resource-details (&optional describe)
+  "Get the details of the resource under the cursor.
+
+ DESCRIBE is the optional param to describe instead of get."
+  (interactive "P")
+  (let* ((resource (kubel--get-resource-under-cursor))
+         (process-name (format "kubel - %s - %s" kubel-resource resource)))
+    (if describe
+        (kubel--exec process-name (list "describe" kubel-resource (kubel--get-resource-under-cursor)))
+      (kubel--exec process-name (list "get" kubel-resource (kubel--get-resource-under-cursor) "-o" kubel-output)))
+    (when (or (string-equal kubel-output "yaml") (transient-args 'kubel-describe-popup))
+      (yaml-mode)
+      (kubel-yaml-editing-mode))
+    (goto-char (point-min))))
+
+
+(defun kubel--default-tail-arg (args)
+  "Ugly function to make sure that there is at least the default tail.
+
+ARGS is the arg list from transient."
+  (if (car (remove nil (mapcar (lambda (x)
+                                 (string-prefix-p "--tail=" x)) args)))
+      args
+    (append args (list (concat "--tail=" (format "%s" kubel-log-tail-n))))))
+
+(defun kubel-get-pod-logs (&optional args type)
+  "Get the last N logs of the pod under the cursor.
+
+ARGS is the arguments list from transient.
+TYPE is containers or initContainers."
+  (interactive
+   (list (transient-args 'kubel-log-popup)))
+  (dolist (pod (if (kubel--is-pod-view)
+                   (if (kubel--items-selected-p)
+                       kubel--selected-items
+                     (list (kubel--get-resource-under-cursor)))
+                 (list (kubel--select-resource "Pods"))))
+    (let* ((type (or type "containers"))
+           (containers (kubel--get-containers pod type))
+           (container (if (equal (length containers) 1)
+                          (car containers)
+                        (completing-read "Select container: " containers)))
+           (process-name (format "kubel - logs - %s - %s" pod container)))
+      (kubel--exec process-name
+                   (append '("logs") (kubel--default-tail-arg args) (list pod container)) t))))
+
+(defun kubel-get-pod-logs--initContainer (&optional args)
+  "Get the last N logs of the pod under the cursor.
+
+ARGS is the arguments list from transient."
+  (interactive
+   (list (transient-args 'kubel-log-popup)))
+  (kubel-get-pod-logs args "initContainers"))
+
+(defun kubel-get-logs-by-labels (&optional args)
+  "Get the last N logs of the pods by labels.
+ARGS is the arguments list from transient."
+  (interactive
+   (list (transient-args 'kubel-log-popup)))
+  (let* ((labels (kubel--get-pod-labels))
+         (label (completing-read "Select container: " labels))
+         (process-name (format "kubel - logs - %s" label)))
+    (kubel--exec process-name
+                 (append '("logs") (kubel--default-tail-arg args) '("-l") (list label)) t)))
+
+(defun kubel-copy-resource-name ()
+  "Copy the name of the pod under the cursor."
+  (interactive)
+  (kill-new (kubel--get-resource-under-cursor))
+  (message "Resource name copied to kill-ring"))
+
+(defun kubel-copy-log-command ()
+  "Copy the streaming log command of the pod under the cursor."
+  (interactive)
+  (kill-new
+   (format "%s logs -f --tail=%s %s"
+           (kubel--get-command-prefix)
+           kubel-log-tail-n
+           (if (kubel--is-pod-view)
+               (kubel--get-resource-under-cursor)
+             (kubel--select-resource "Pods"))))
+  (message "Log command copied to kill-ring"))
+
+(defun kubel-copy-command-prefix ()
+  "Copy the kubectl command prefix."
+  (interactive)
+  (kill-new (kubel--get-command-prefix))
+  (message "Command prefix copied to kill-ring"))
+
+(defun kubel-copy-last-command ()
+  "Copy the last kubectl command ran."
+  (interactive)
+  (kill-new kubel--last-command)
+  (message (concat "Last command copied: " kubel--last-command)))
+
+(defun kubel-set-kubectl-config-file (configfile)
+  "Set the path to the kubectl CONFIGFILE."
+  (interactive "f")
+  (let ((configfile (or configfile "~/.kube/config")))
+    (if (file-exists-p (expand-file-name configfile))
+           (setenv "KUBECONFIG" (expand-file-name configfile))
+      (error "Kubectl config file '%s' does not exist!" configfile))))
+
+(defun kubel--can-get-namespace ()
+  "Determine if permissions allow for `kubectl get namespace` in current context."
+  (cond ((eq kubel-use-namespace-list 'on) t)
+        ((eq kubel-use-namespace-list 'auto)
+         (progn
+           (unless kubel--can-get-namespace-cached
+             (setq kubel--can-get-namespace-cached
+                   (equal "yes\n"
+                          (kubel--exec-to-string
+                           (format "kubectl --context %s auth can-i list namespaces" kubel-context))))))
+         kubel--can-get-namespace-cached)))
+
+(defun kubel--get-namespace ()
+  "Get namespaces for current context, try to recover from cache first."
+  (unless kubel--namespace-list-cached
+    (setq kubel--namespace-list-cached
+          (split-string (kubel--exec-to-string
+                         (format "kubectl --context %s get namespace -o jsonpath='{.items[*].metadata.name}'" kubel-context)) " ")))
+  kubel--namespace-list-cached)
+
+(defun kubel--list-namespace ()
+  "List namespace, either from history, or dynamically if possible."
+  (if (kubel--can-get-namespace)
+      (kubel--get-namespace)
+    kubel-namespace-history))
+
+(defun kubel--add-namespace-to-history (namespace)
+  "Add NAMESPACE to history if it isn't there already."
+  (unless (member namespace kubel-namespace-history)
+    (push namespace kubel-namespace-history)))
+
+(defun kubel-set-namespace ()
+  "Set the namespace."
+  (interactive)
+  (let* ((namespace (completing-read "Namespace: " (kubel--list-namespace)
+                                     nil nil nil nil "default"))
+            (kubel--buffer (get-buffer (kubel--buffer-name)))
+            (last-default-directory (when kubel--buffer
+                                                  (with-current-buffer kubel--buffer default-directory))))
+    (when kubel--buffer (kill-buffer kubel--buffer))
+    (setq kubel-namespace namespace)
+    (kubel--add-namespace-to-history namespace)
+    (kubel last-default-directory)))
+
+(defun kubel-set-context ()
+  "Set the context."
+  (interactive)
+  (let* ((kubel--buffer (get-buffer (kubel--buffer-name)))
+            (last-default-directory (when kubel--buffer (with-current-buffer kubel--buffer default-directory))))
+    (when kubel--buffer (kill-buffer kubel--buffer));; kill buffer for previous context if possible
+    (setq kubel-context
+          (completing-read
+           "Select context: "
+           (split-string (kubel--exec-to-string "kubectl config view -o jsonpath='{.contexts[*].name}'") " ")))
+    (kubel--invalidate-context-caches)
+    (setq kubel-namespace "default")
+    (kubel last-default-directory)))
+
+(defun kubel--fetch-api-resource-list ()
+  "Fetch the API resource list."
+  (split-string (kubel--exec-to-string
+                ( format "kubectl --context %s api-resources -o name --no-headers=true" kubel-context )) "\n"))
+
+(defun kubel-set-resource (&optional refresh)
+  "Set the resource.
+If called with a prefix argument REFRESH, refreshes
+the context caches, including the cached resource list."
+  (interactive "P")
+  (when refresh (kubel--invalidate-context-caches))
+  (let* ((current-buffer-name (kubel--buffer-name))
+         (resource-list (if (kubel-kubernetes-compatible-p '(1 13 3))
+                               (if (null kubel--kubernetes-resources-list-cached)
+                                               (setq kubel--kubernetes-resources-list-cached
+                                      (kubel--fetch-api-resource-list))
+                              kubel--kubernetes-resources-list-cached)
+                             kubel-kubernetes-resources-list))
+            (kubel--buffer (get-buffer current-buffer-name))
+            (last-default-directory (when kubel--buffer (with-current-buffer kubel--buffer default-directory))))
+    (setq kubel-resource
+             (completing-read "Select resource: " resource-list))
+    (when kubel--buffer (kill-buffer kubel--buffer)) ;; kill buffer for previous context if possible
+    (kubel last-default-directory)))
+
+(defun kubel-set-output-format ()
+  "Set output format of kubectl."
+  (interactive)
+  (setq kubel-output
+           (completing-read
+            "Set output format: "
+            '("yaml" "json" "wide" "custom-columns=")))
+  (kubel))
+
+(defun kubel-port-forward-pod (p)
+  "Port forward a pod to your local machine.
+
+P can be a single number or a localhost:container port pair."
+  (interactive "sPort: ")
+  (let* ((port (if (string-match-p ":" p) p (format "%s:%s" p p)))
+         (pod (if (kubel--is-pod-view)
+                  (kubel--get-resource-under-cursor)
+                (kubel--select-resource "Pods")))
+         (process-name (format "kubel - port-forward - %s:%s" pod port)))
+    (kubel--exec process-name (list "port-forward" pod port))))
+
+(defun kubel-setup-tramp ()
+  "Setup a kubectl TRAMP."
+  (setq tramp-methods (delete (assoc "kubectl" tramp-methods) tramp-methods)) ;; cleanup previous tramp method
+  ;; TODO error message if resource is not pod
+  (add-to-list 'tramp-methods
+               `("kubectl"
+                 (tramp-login-program      "kubectl")
+                 (tramp-login-args         (,(kubel--get-context-namespace) ("exec" "-it") ("-u" "%u") ("%h") ("sh")))
+                 (tramp-remote-shell       "sh")
+                 (tramp-remote-shell-args  ("-i" "-c"))))) ;; add the current context/namespace to tramp methods
+
+(defun kubel-exec-pod ()
+  "Exec into the pod under the cursor -> `find-file."
+  (interactive)
+  (kubel-setup-tramp)
+  (setq dir-prefix (or
+                           (when (tramp-tramp-file-p default-directory)
+                             (with-parsed-tramp-file-name default-directory nil
+                                   (format "%s%s:%s@%s|" (or hop "") method user host)))
+                           ""))
+  (find-file (format "/%skubectl:%s:/" dir-prefix (if (kubel--is-pod-view)
+                                                                             (kubel--get-resource-under-cursor)
+                                                                           (kubel--select-resource "Pods")))))
+
+(defun kubel-exec-shell-pod ()
+  "Exec into the pod under the cursor -> shell."
+  (interactive)
+  (kubel-setup-tramp)
+  (let* ((dir-prefix (or
+                    (when (tramp-tramp-file-p default-directory)
+                      (with-parsed-tramp-file-name default-directory nil
+                        (format "%s%s:%s@%s|" (or hop "") method user host))) ""))
+         (pod (if (kubel--is-pod-view)
+                  (kubel--get-resource-under-cursor)
+                (kubel--select-resource "Pods")))
+         (default-directory (format "/%skubectl:%s:/" dir-prefix pod)))
+    (shell (format "*kubel - shell - %s*" pod))))
+
+(defun kubel-exec-eshell-pod ()
+  "Exec into the pod under the cursor -> eshell."
+  (interactive)
+  (kubel-setup-tramp)
+  (let* ((dir-prefix (or
+                    (when (tramp-tramp-file-p default-directory)
+                      (with-parsed-tramp-file-name default-directory nil
+                        (format "%s%s:%s@%s|" (or hop "") method user host))) ""))
+         (pod (if (kubel--is-pod-view)
+                  (kubel--get-resource-under-cursor)
+                (kubel--select-resource "Pods")))
+         (default-directory (format "/%skubectl:%s:/" dir-prefix pod))
+         (eshell-buffer-name (format "*kubel - eshell - %s*" pod)))
+    (eshell)))
+
+(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-set-filter (filter)
+  "Set the pod filter.
+
+FILTER is the filter string."
+  (interactive "MFilter: ")
+  (setq kubel-resource-filter filter)
+  (kubel))
+
+(defun kubel--jump-to-highlight (init search reset)
+  "Base function to jump to highlight.
+
+INIT is to be called before searching.
+SEARCH is to apply the search and can be repeated safely.
+RESET is to be called if the search is nil after the first attempt."
+  (unless (equal kubel-resource-filter "")
+    (funcall init)
+    (unless (funcall search)
+      (funcall reset)
+      (funcall search))
+    (beginning-of-line)))
+
+(defun kubel-jump-to-next-highlight ()
+  "Jump to the next hightlighted resrouce."
+  (interactive)
+  (kubel--jump-to-highlight
+   #'end-of-line
+   (lambda () (re-search-forward kubel-resource-filter (point-max) t))
+   #'beginning-of-buffer))
+
+(defun kubel-jump-to-previous-highlight ()
+  "Jump to the previou highlighted resrouce."
+  (interactive)
+  (kubel--jump-to-highlight
+   #'beginning-of-line
+   (lambda () (re-search-backward kubel-resource-filter (point-min) t))
+   #'end-of-buffer))
+
+(defun kubel-rollout-history ()
+  "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)))))
+
+(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)))))
+
+(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))
+
+(defun kubel-unmark-all ()
+  "Unmark all items."
+  (interactive)
+  (setq kubel--selected-items '())
+  (kubel))
+
+;; popups
+
+(define-transient-command kubel-exec-popup ()
+  "Kubel Exec Menu"
+  ["Actions"
+   ("d" "Dired" kubel-exec-pod)
+   ("e" "Eshell" kubel-exec-eshell-pod)
+   ("s" "Shell" kubel-exec-shell-pod)])
+
+(define-transient-command 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)])
+
+(define-transient-command 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)])
+
+(define-transient-command kubel-delete-popup ()
+  "Kubel Delete menu"
+  ["Arguments"
+   ("-f" "Force" "--force --grace-period=0")]
+  ["Actions"
+   ("k" "Delete resource(s)" kubel-delete-resource)])
+
+(define-transient-command kubel-describe-popup ()
+  "Kubel Describe Menu"
+  ["Arguments"
+   ("-y" "Yaml" "-o yaml")]
+  ["Actions"
+   ("RET" "Describe" kubel-get-resource-details)])
+
+(define-transient-command kubel-help-popup ()
+  "Kubel Menu"
+  [["Actions"
+    ;; global
+    ("RET" "Resource details" kubel-describe-popup)
+    ("E" "Quick edit" kubel-quick-edit)
+    ("g" "Refresh" kubel)
+    ("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)]
+   ["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)]
+   ["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)
+    (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)
+    ;; based on view
+    (define-key map (kbd "p") 'kubel-port-forward-pod)
+    (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 "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'.")
+
+(defvar kubel-last-position nil)
+
+;;;###autoload
+(defun kubel (&optional directory)
+  "Invoke the kubel buffer.
+
+DIRECTORY is optional for TRAMP support."
+  (interactive)
+  (kubel--save-line)
+  (kubel--pop-to-buffer (kubel--buffer-name))
+  (when directory (setq default-directory directory))
+  (kubel-mode)
+  (message (concat "Namespace: " kubel-namespace)))
+
+(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)
+  (let ((entries (kubel--populate-list)))
+    (setq tabulated-list-format (car entries))
+    (setq tabulated-list-entries (cadr entries)))   ; TODO handle "No resource found"
+  (setq tabulated-list-sort-key kubel--list-sort-key)
+  (setq tabulated-list-sort-key nil)
+  (tabulated-list-init-header)
+  (tabulated-list-print)
+  (hl-line-mode 1)
+  (run-mode-hooks 'kubel-mode-hook))
+
+(add-hook 'kubel-mode-hook #'kubel--jump-back-to-line)
+
+(provide 'kubel)
+;;; kubel.el ends here
diff --git a/elpa/kubel-readme.txt b/elpa/kubel-readme.txt
new file mode 100644 (file)
index 0000000..a1c1cc1
--- /dev/null
@@ -0,0 +1 @@
+Emacs extension for controlling Kubernetes with limited permissions.