From e6842e780e5dcd3fd482eb8c3541f2f68d6fbdd0 Mon Sep 17 00:00:00 2001 From: Andreas Gerler Date: Wed, 10 Feb 2021 15:01:51 +0100 Subject: [PATCH] add kubel including my fix for fetch-api-resource-list --- elpa/kubel-20201223.1730/kubel-autoloads.el | 29 + elpa/kubel-20201223.1730/kubel-pkg.el | 2 + elpa/kubel-20201223.1730/kubel.el | 1103 +++++++++++++++++++ elpa/kubel-readme.txt | 1 + 4 files changed, 1135 insertions(+) create mode 100644 elpa/kubel-20201223.1730/kubel-autoloads.el create mode 100644 elpa/kubel-20201223.1730/kubel-pkg.el create mode 100644 elpa/kubel-20201223.1730/kubel.el create mode 100644 elpa/kubel-readme.txt diff --git a/elpa/kubel-20201223.1730/kubel-autoloads.el b/elpa/kubel-20201223.1730/kubel-autoloads.el new file mode 100644 index 00000000..c04476a1 --- /dev/null +++ b/elpa/kubel-20201223.1730/kubel-autoloads.el @@ -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)))) + + +;;;### (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-"))) + +;;;*** + +;; 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 index 00000000..86000bee --- /dev/null +++ b/elpa/kubel-20201223.1730/kubel-pkg.el @@ -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 index 00000000..d1741d8d --- /dev/null +++ b/elpa/kubel-20201223.1730/kubel.el @@ -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 ) +;; 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 index 00000000..a1c1cc10 --- /dev/null +++ b/elpa/kubel-readme.txt @@ -0,0 +1 @@ +Emacs extension for controlling Kubernetes with limited permissions. -- 2.47.3