From: Eshel Yaron Date: Sun, 28 Jul 2024 10:16:03 +0000 (+0200) Subject: Initial commit X-Git-Tag: v0.2.0~48 X-Git-Url: http://git.eshelyaron.com/gitweb/?a=commitdiff_plain;h=d7a4f0d5abebf70437685c7e7947c7079ddbc21a;p=kubed.git Initial commit --- d7a4f0d5abebf70437685c7e7947c7079ddbc21a diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..14646b3 --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,4 @@ +((emacs-lisp-mode + . ((fill-column . 72) + (emacs-lisp-docstring-fill-column . 72) + (indent-tabs-mode)))) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b25c15b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..77abeb3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Eshel Yaron + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NEWS.org b/NEWS.org new file mode 100644 index 0000000..947db1f --- /dev/null +++ b/NEWS.org @@ -0,0 +1,15 @@ +#+title: Kubed NEWS -- history of user-visible changes +#+author: Eshel Yaron +#+email: me@eshelyaron.com +#+language: en +#+options: ':t toc:nil num:nil ^:{} + +This file contains the release notes for Kubed, a rich Emacs interface +for Kubernetes. + +For further details, see the Kubed manual: +[[https://eshelyaron.com/sweep.html][https://eshelyaron.com/kubed.html]]. + +* Version 0.1.0 on 2024-08-01 + +Initial release. diff --git a/README.md b/README.md new file mode 100644 index 0000000..28ab390 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Kubed + +Kubed is a rich Kubernetes interface within Emacs. It helps you work +with your Kubernetes clusters and deployments with the full power of +`kubectl`, and with the comfort and confidence of an intuitive +interactive interface. + +![Kubed](kubed.png) + +You can use Kubed to: + +- Browse and manage Kubernetes workloads +- Connect to pods and edit files or execute commands +- Create new resources, edit and delete them +- Get help about various Kubernetes objects +- ... + +# Getting Started + +Use your favorite Emacs package manager to install Kubed from Git. You +can clone the Kubed Git repository from any of the following locations: + +- https://git.sr.ht/~eshel/kubed +- https://github.com/eshelyaron/kubed.git +- git://git.eshelyaron.com/kubed.git + +To get started with Kubed, all you need is `kubectl` and Emacs. For +information about usage and customization, see the Kubed manual in Info +or online at https:/eshelyaron.com/kubed.html diff --git a/cobra.el b/cobra.el new file mode 100644 index 0000000..8efb3e2 --- /dev/null +++ b/cobra.el @@ -0,0 +1,136 @@ +;;; cobra.el --- Complete Cobra command lines -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Eshel Yaron + +;; Author: Eshel Yaron +;; Keywords: tools + +;;; Commentary: + +;; Cobra is a popular Golang framework for CLI programs. This library +;; defines function `cobra-read-command-line', which helps you read a +;; command line for a program that uses Cobra, with completion. +;; Prominent examples of Cobra programs are `kubectl' and `docker'. + +;;; Code: + +(defvar cobra--cache nil) + +(defun cobra-completion-table (executable s p a) + "Completion table for command lines that invoke EXECUTABLE. + +Perform completion action A on string S with predicate P." + (let ((start 0)) + (while (string-match "[[:space:]=]" s start) + (setq start (match-end 0))) + (if (eq a 'metadata) + `(metadata + (category . cobra-command-line) + (affixation-function + . ,(lambda (cands) + (let ((max (seq-max + (cons 0 (mapcar #'string-width cands))))) + (mapcar + (lambda (cand) + (list + cand "" + (if-let + ((desc (get-text-property + 0 'cobra-argument-description + cand))) + (concat + (make-string (1+ (- max (string-width cand))) ?\s) + (propertize desc 'face 'completions-annotations)) + ""))) + cands))))) + (let* ((lines + (cdr + (if (string= s (car cobra--cache)) + ;; Cache hit. + cobra--cache + (setq + cobra--cache + (cons s + (apply #'process-lines-ignore-status + executable "__complete" + (let ((args (cdr (split-string-and-unquote s)))) + (if (string-suffix-p " " s) + ;; Restore omitted empty argument. + (nconc args '("")) + args)))))))) + (code nil) + (comps (seq-take-while + (lambda (line) + (not (and (string-match "^:\\([0-9]+\\)$" line) + (setq code (string-to-number + (match-string 1 line)))))) + lines))) + ;; `code' encodes "completion directives", as follows: + ;; #b000001: An error occurred, ignore completions. + ;; #b000010: Don't add space after completion. + ;; #b000100: Don't fall back to file completion. + ;; #b001000: Completions are really file extension filters. + ;; #b010000: Complete directory names. + ;; #b100000: Preserve completions order. + (when (and code (zerop (logand 1 code))) + ;; Error bit in unset, proceed. + (if (= #b100 (logand #b100 code)) + ;; No file name completion. + (if (eq (car-safe a) 'boundaries) + `(boundaries + ,start . ,(and (string-match "[[:space:]=]" (cdr a)) + (match-beginning 0))) + (let ((table + (mapcar + ;; Annotate completion candidates. + (lambda (comp) + (pcase (split-string comp "\t" t) + (`(,c ,d . ,_) + (propertize + c 'cobra-argument-description + ;; Only keep first sentence. + (car (split-string d "\\." t)))) + (`(,c . ,_) c))) + comps))) + (if a (complete-with-action a table (substring s start) p) + ;; `try-completion'. + (let ((comp (complete-with-action a table (substring s start) p))) + (if (stringp comp) (concat (substring s 0 start) comp) comp))))) + ;; File name completion. + (setq p + (cond + ((= #b1000 (logand #b1000 code)) + ;; `comps' are valid extensions. + (lambda (f) + (or (file-directory-p f) + (when (string-match "\\.[^.]*\\'" f) + (member (substring f (1+ (match-beginning 0))) + comps))))) + ((= #b10000 (logand #b10000 code)) + ;; Directory name completion. + #'file-directory-p))) + (if (eq (car-safe a) 'boundaries) + ;; Find nested boundaries. + (let* ((suf (cdr a)) + (bounds (completion-boundaries + (substring s start) #'completion-file-name-table p + (substring suf 0 (string-match "[[:space:]=]" suf))))) + `(boundaries ,(+ (car bounds) start) . ,(cdr bounds))) + (if a (complete-with-action a #'completion-file-name-table + (substring s start) p) + (let ((comp (complete-with-action a #'completion-file-name-table + (substring s start) p))) + (if (stringp comp) (concat (substring s 0 start) comp) comp)))))))))) + +;;;###autoload +(defun cobra-read-command-line (prompt initial &optional hist) + "Prompt with PROMPT for a command line starting with INITIAL. + +Optional argument HIST is the name of the history list variable to use, +if it is nil or omitted, it defaults to `shell-command-history'." + (let ((exec (car (split-string-and-unquote initial)))) + (completing-read prompt (apply-partially #'cobra-completion-table exec) + nil nil initial (or hist 'shell-command-history)))) + +(provide 'cobra) +;;; cobra.el ends here diff --git a/kubed-transient.el b/kubed-transient.el new file mode 100644 index 0000000..7154d18 --- /dev/null +++ b/kubed-transient.el @@ -0,0 +1,226 @@ +;;; kubed-transient.el --- Kubernetes transient menus -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Eshel Yaron + +;; Author: Eshel Yaron +;; Keywords: tools + +;;; Commentary: + +;; This library extends Kubed with transient menus for various +;; Kubernetes operations. + +;;; Code: + +(require 'kubed) +(require 'transient) + +(defclass kubed-transient-infix (transient-infix) ()) + +(defun kubed-transient-read-namespace (prompt _initial-input _history) + "Prompt with PROMPT for Kubernetes namespace." + (kubed-read-namespace prompt (kubed-current-namespace))) + +(defun kubed-transient-read-ingressclass (prompt _initial-input _history) + "Prompt with PROMPT for Kubernetes ingress class." + (kubed-read-ingressclass prompt)) + +(defun kubed-transient-read-service-and-port (prompt _initial-input _history) + "Prompt with PROMPT for Kubernetes service and port number." + (let ((service (kubed-read-service prompt))) + (concat service ":" (number-to-string (read-number "Port number: "))))) + +(defun kubed-transient-read-resource-definition-file-name + (_prompt _initial-input _history) + "Read and return Kubernetes resource definition file name." + (kubed-read-resource-definition-file-name)) + +;;;###autoload +(transient-define-prefix kubed-transient () + "Perform Kubernetes operation." + ["Actions" + ("+" "Create" kubed-transient-create) + ("*" "Apply" kubed-transient-apply) + ("r" "Run" kubed-transient-run) + ("a" "Attach" kubed-transient-attach) + ("d" "Diff" kubed-transient-diff) + ("e" "Exec" kubed-transient-exec) + ("E" "Explain" kubed-explain) + ("!" "Command line" kubed-kubectl-command)]) + +;;;###autoload +(transient-define-prefix kubed-transient-attach () + "Attach to running process in container in Kubernetes pod." + ["Switches" + ("-i" "Open stdin" "--stdin") + ("-t" "Allocate TTY" "--tty")] + ["Options" + ("-n" "Namespace" "--namespace=" + :prompt "Namespace" :reader kubed-transient-read-namespace)] + ["Actions" + ("a" "Attach" kubed-attach) + ("!" "Command line" kubed-kubectl-command)] + (interactive) + (transient-setup 'kubed-transient-attach nil nil + :value '("--stdin" "--tty") + :scope '("attach"))) + +;;;###autoload +(transient-define-prefix kubed-transient-diff () + "Display difference between Kubernetes resource definition and current state." + ["Switches" + ("-M" "Include managed fields" "--show-managed-fields")] + ["Options" + ("-f" "Definition file" "--filename=" + :reader kubed-transient-read-resource-definition-file-name)] + ["Actions" + ("d" "Diff" kubed-diff) + ("!" "Command line" kubed-kubectl-command)] + (interactive) + (transient-setup 'kubed-transient-diff nil nil + :scope '("diff"))) + +;;;###autoload +(transient-define-prefix kubed-transient-exec () + "Execute command in Kubernetes pod." + ["Switches" + ("-i" "Open stdin" "--stdin") + ("-t" "Allocate TTY" "--tty")] + ["Options" + ("-n" "Namespace" "--namespace=" + :prompt "Namespace" :reader kubed-transient-read-namespace) + ("--" "Command" "-- =" + :prompt "Command: ")] + ["Actions" + ("x" "Execute" kubed-exec) + ("!" "Command line" kubed-kubectl-command)] + (interactive) + (transient-setup 'kubed-transient-exec nil nil + :value '("--stdin" "--tty") + :scope '("exec"))) + +;;;###autoload +(transient-define-prefix kubed-transient-run () + "Run container image in a Kubernetes pod." + ["Switches" + ("-A" "Attach" "--attach") + ("-i" "Open stdin" "--stdin") + ("-t" "Allocate TTY" "--tty") + ("-R" "Remove after exit" "--rm") + ("-C" "Override container command" "--command")] + ["Options" + ("-n" "Namespace" "--namespace=" + :prompt "Namespace" :reader kubed-transient-read-namespace) + ("-I" "Image" "--image=" + :prompt "Image to deploy: ") + ("-p" "Port" "--port=" + :prompt "Port to expose: " :reader transient-read-number-N+) + ("-E" "Env vars" "--env=" + :prompt "Set environment VAR=VAL: " + :multi-value repeat) + ("--" "Arguments" "-- =" + :prompt "Arguments for container command: ")] + ["Actions" + ("r" "Run" kubed-run) + ("!" "Command line" kubed-kubectl-command)] + (interactive) + (transient-setup 'kubed-transient-run nil nil + :scope '("run"))) + +;;;###autoload +(transient-define-prefix kubed-transient-apply () + "Apply configuration to Kubernetes resource." + ["Options" + ("-f" "Definition file" "--filename=" + :reader kubed-transient-read-resource-definition-file-name)] + ["Actions" + ("*" "apply" kubed-apply) + ("!" "Command line" kubed-kubectl-command)] + (interactive) + (transient-setup 'kubed-transient-apply nil nil + :scope '("apply"))) + +;;;###autoload +(transient-define-prefix kubed-transient-create () + "Create Kubernetes resource." + ["Options" + ("-f" "Definition file" "--filename=" + :reader kubed-transient-read-resource-definition-file-name)] + ["Kinds" + ("d" "deployment" kubed-transient-create-deployment) + ("n" "namespace" kubed-create-namespace) + ("c" "cronjob" kubed-transient-create-cronjob) + ("i" "ingress" kubed-transient-create-ingress)] + ["Actions" + ("+" "Create" kubed-create) + ("!" "Command line" kubed-kubectl-command)] + (interactive) + (transient-setup 'kubed-transient-create nil nil + :scope '("create"))) + +;;;###autoload +(transient-define-prefix kubed-transient-create-cronjob () + "Create Kubernetes cronjob." + ["Options" + ("-n" "Namespace" "--namespace=" + :prompt "Namespace" :reader kubed-transient-read-namespace) + ("-I" "Image" "--image=" + :prompt "Image to run: ") + ("-S" "Schedule" "--schedule=" + :prompt "Cron schedule: ") + ("--" "Command" "-- =" + :prompt "Command: ")] + ["Actions" + ("+" "Create" kubed-create-cronjob) + ("!" "Command line" kubed-kubectl-command)] + (interactive) + (transient-setup 'kubed-transient-create-cronjob nil nil + :scope '("create" "cronjob"))) + +;;;###autoload +(transient-define-prefix kubed-transient-create-ingress () + "Create Kubernetes ingress." + ["Options" + ("-n" "Namespace" "--namespace=" + :prompt "Namespace" :reader kubed-transient-read-namespace) + ("-c" "Class" "--class=" + :prompt "Class" :reader kubed-transient-read-ingressclass) + ("-d" "Default backend service" "--default-backend=" + :prompt "Default backend service" + :reader kubed-transient-read-service-and-port) + ("-a" "Annotation" "--annotation=" + :prompt "Ingress annotations: " + :multi-value repeat) + ("-r" "Rule" "--rule=" + :prompt "Ingress rule: ")] + ["Actions" + ("+" "Create" kubed-create-ingress) + ("!" "Command line" kubed-kubectl-command)] + (interactive) + (transient-setup 'kubed-transient-create-ingress nil nil + :scope '("create" "ingress"))) + +;;;###autoload +(transient-define-prefix kubed-transient-create-deployment () + "Create Kubernetes deployment." + ["Options" + ("-n" "Namespace" "--namespace=" + :prompt "Namespace" :reader kubed-transient-read-namespace) + ("-r" "Replicas" "--replicas=" + :prompt "Number of replicas: " :reader transient-read-number-N+) + ("-I" "Image" "--image=" + :prompt "Images to deploy: " + :multi-value repeat) + ("-p" "Port" "--port=" + :prompt "Port to expose: " :reader transient-read-number-N+) + ("--" "Command" "-- =" + :prompt "Command: ")] + ["Actions" + ("+" "Create" kubed-create-deployment) + ("!" "Command line" kubed-kubectl-command)] + (interactive) + (transient-setup 'kubed-transient-create-deployment nil nil + :scope '("create" "deployment"))) + +(provide 'kubed-transient) +;;; kubed-transient.el ends here diff --git a/kubed.el b/kubed.el new file mode 100644 index 0000000..03b13a1 --- /dev/null +++ b/kubed.el @@ -0,0 +1,1908 @@ +;;; kubed.el --- Kubernetes, Emacs, done! -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Eshel Yaron + +;; Author: Eshel Yaron +;; Maintainer: Eshel Yaron <~eshel/kubed-devel@lists.sr.ht> +;; Keywords: tools kubernetes containers +;; URL: https://eshelyaron.com +;; Package-Version: 0.1.0 +;; Package-Requires: ((emacs "29.1")) + +;;; Commentary: + +;; This library defines commands for interacting with Kubernetes +;; resources, such as Kuberenetes pods, services, deployments, and more. +;; +;; Use `kubed-display-pod' to display a Kuberenetes pod, +;; `kubed-edit-pod' to edit it, `kubed-delete-pods' to delete it, and +;; `kubed-list-pods' to see a menu of all pods. You can create new pods +;; from YAML or JSON files with `kubed-create-pod'. To update the list +;; of current pods, use `kubed-update-pods' or `kubed-update-all'. +;; +;; Similar commands are defined for other types of resources as well. +;; +;; This library interacts with Kuberenetes via `kubectl', and uses the +;; current `kubectl' context and namespace. To change your current +;; Kuberenetes context or namespace, use `kubed-use-context' and +;; `kubed-set-namespace'; all resource lists are updated automatically +;; after you do so. In addition, you can use the minor mode +;; `kubed-all-namespaces-mode' to see resources from all namespaces. +;; The prefix keymap `kubed-prefix-map' gives you quick access to these +;; and other useful commands. You may want to bind it to a convenient +;; key in your global keymap with `keymap-global-set': +;; +;; (keymap-global-set "C-c k" 'kubed-prefix-map) +;; +;; If you want to work with more or different types of Kubernetes +;; resources, use the macro `kubed-define-resource'. This macro defines +;; some common functions and commands that'll get you started with ease. +;; +;; You may also want to try out the companion library `kubed-transient', +;; which provides transient menus for some of the commands defined here. + +;;; Todo: + +;; - Support filtering resource lists. +;; - Add menu bar and context menus. + +;;; Code: + +(defgroup kubed nil + "Kubernetes interface." + :group 'tools) + +(defcustom kubed-update-hook nil + "List of functions that `kubed-update-all' runs." + :type 'hook) + +(defcustom kubed-kubectl-program "kubectl" + "Name of `kubectl' executable to use for interacting with Kubernetes." + :type 'string) + +(defcustom kubed-yaml-setup-hook '(yaml-ts-mode view-mode) + "List of functions to call in Kubernetes resource description YAML buffers. + +The first function in the list should normally be the major mode to use, +by default it is `yaml-ts-mode'." + :type 'hook) + +(defcustom kubed-logs-setup-hook '(view-mode) + "List of functions to call when setting up Kubernetes pod logs buffers." + :type 'hook) + +;;;###autoload +(defun kubed-update-all () + "Update all Kuberenetes resource lists." + (interactive) + (run-hooks 'kubed-update-hook)) + +(defvar-local kubed-frozen nil + "Whether the current buffer shows a frozen list of Kuberenetes resources. + +If a resource lists is frozen, then Emacs does not update it when +obtaining new information from Kuberenetes clusters.") + +(defcustom kubed-name-column '("Name" 48 t) + "Specification of name column in Kubernetes resource list buffers." + :type '(list string natnum boolean)) + +(defcustom kubed-namespace-column '("Namespace" 12 t) + "Specification of namespace column in Kubernetes resource list buffers." + :type '(list string natnum boolean)) + +;;;###autoload +(define-minor-mode kubed-all-namespaces-mode + "Show Kubernetes resources from all namespaces, not just current namespace." + :global t + (message "Kubed \"all namespaces\" mode is now %s" + (if kubed-all-namespaces-mode "ON" "OFF"))) + +;;;###autoload +(defmacro kubed-define-resource (resource &optional properties &rest commands) + "Define Kubernetes RESOURCE with associated PROPERTIES and COMMANDS. + +RESOURCE is a symbol corresponding to a Kubernetes resource type, such +as `pod' or `service'. This macro defines the following commands for +interacting with Kubernetes RESOURCEs: + +- `kubed-display-RESROURCE': prompt for a RESOURCE and display its + description in YAML format. See also `kubed-yaml-setup-hook'. +- `kubed-edit-RESROURCE': prompt for a RESOURCE and edit it. +- `kubed-delete-RESROURCE': prompt for a RESOURCE and delete it. +- `kubed-list-RESROURCEs': display a buffer listing all RESOURCEs in the + current namespace. The RESOURCEs list buffer uses a dedicated major + mode, `kubed-RESOURCEs-mode', which is also defined by this macro. +- `kubed-update-RESROURCEs': update and repopulate RESOURCEs list. +- `kubed-create-RESROURCE': create a RESOURCE from a YAML or a JSON file. +- `kubed-explain-RESROURCEs': show buffer with help about RESOURCEs. + +This macro also defines a prefix keymap, `kubed-RESOURCE-prefix-map', +with bindings for the above commands. + +PROPERTIES is a list of lists (PROP JSON-PATH WIDTH SORT FORMAT . ATTRS) +that specify properties of RESOURCEs. PROP is the name of the property, +as a symbol; JSON-PATH is a JSONPath expression that evaluates to the +value of PROP when applied to the JSON representation of a RESOURCE. +WIDTH, SORT, FORMAT and ATTRS are optional and can be omitted. WIDTH is +used as the default width of the column corresponding to PROP in +RESOURCEs list buffers; SORT is sort predicate, a function that takes +two values of PROP as strings and return non-nil if the first should +sort before the second; FORMAT is a function that takes a value of PROP +and formats it; and ATTRS is a plist of additional attributes of the +PROP column, see `tabulated-list-format' for available attributes. For +example, (phase \".status.phase\" 10) says that RESOURCE has a `phase' +property at JSONPath \".status.phase\" whose values are typically 10 +columns wide. The first property in PROPERTIES, is used to annotate +completion candidates when prompting for a RESOURCE. + +COMMANDS is a list of elements (COMMAND KEYS DOC-PREFIX . BODY) that +define commands for RESOURCE list buffers. COMMAND is a symbol +specifying the suffix of the command name, the full name of the command +is `kubed-RESOURCEs-COMMAND' (for example, `kubed-pods-shell'); KEYS is +either a string that specifies a key sequence to bind to the command in +`kubed-RESOURCEs-mode-map', or nil if the command should not be bound; +DOC-PREFIX is a string used to construct the docstring of the command, +this macro appends the string \" Kubernetes RESOURCE at point.\" to it +to obtain the final docstring; lastly, BODY is the body the command. +Within BODY, the variable RESOURCE is let-bound to the name of the +RESOURCE at point. If RESOURCE is namespaced, then also the variable +`k8sns' is let-bound to the namespace of the RESOURCE at point within +BODY when `kubed-all-namespaces-mode' is enabled. For example, if +RESOURCE is `pod', the following COMMANDS element defines a command +`kubed-pods-frob' and binds it to the key \"f\" in +`kubed-pods-mode-map': + + (frob \"f\" \"Frobnicate\" + (message \"Preparing...\") + (frobnicate-pod pod k8sns) + (message \"Done.\")) + +By default, this macro assumes that RESOURCE is namespaced. To define a +namespaceless resource type, put `:namespaced nil' before COMMANDS: + + (kubed-define-resource global-thingy (PROP1 PROP2 ...) :namespaced nil + CMD1 + CMD2 + ...) + +Other keyword arguments that go between PROPERTIES and COMMANDS are: + +- `:create (ARGLIST DOCSTRING INTERACTIVE BODY...)': specialize the + resource creation command, `kubed-create-RESROURCE'. ARGLIST, + DOCSTRING, INTERACTIVE and BODY have the same meaning as in `defun'. +- `:prefix (KEY DEFINITION...)': additional keybinding for the prefix + keymap `kubed-RESOURCE-prefix-map'. +- `:plural PLURAL': specify plural form of RESOURCE, as a symbol. If + you omit this keyword argument, the plural form defaults to RESOURCE + followed by \"s\"." + (declare (indent 2)) + (let ((hist-var (intern (format "kubed-%S-history" resource))) + (plrl-var (intern (format "%Ss" resource))) + (read-fun (intern (format "kubed-read-%S" resource))) + (read-nms (intern (format "kubed-read-namespaced-%S" resource))) + (desc-fun (intern (format "kubed-%S-description-buffer" resource))) + (buf-name (format "*kubed-%S*" resource)) + (dsp-name (intern (format "kubed-display-%S" resource))) + (edt-name (intern (format "kubed-edit-%S" resource))) + (crt-name (intern (format "kubed-create-%S" resource))) + (map-name (intern (format "kubed-%S-prefix-map" resource))) + (namespaced t) + (keyword nil) + list-var ents-var hook-var proc-var frmt-var read-crm sure-fun + ents-fun buff-fun frmt-fun affx-fun updt-cmd list-cmd expl-cmd + mark-cmd umrk-cmd exec-cmd list-buf out-name err-name dlt-errb + dlt-name mod-name crt-spec prf-keys) + + ;; Process keyword arguments. + (while (keywordp (car commands)) + (setq keyword (pop commands)) + (cond + ((eq keyword :namespaced) (setq namespaced (pop commands))) + ((eq keyword :create) (setq crt-spec (pop commands))) + ((eq keyword :prefix) (setq prf-keys (pop commands))) + ((eq keyword :plural) (setq plrl-var (pop commands))) + ;; FIXME: Add error for unknown keywords. + (t (pop commands)))) + + (setq list-var (intern (format "kubed-%S" plrl-var)) + ents-var (intern (format "kubed--%S-entries" plrl-var)) + hook-var (intern (format "kubed-update-%S-hook" plrl-var)) + proc-var (intern (format "kubed-%S-process" plrl-var)) + frmt-var (intern (format "kubed-%S-columns" plrl-var)) + read-crm (intern (format "kubed-read-%S" plrl-var)) + sure-fun (intern (format "kubed-ensure-%S" plrl-var)) + ents-fun (intern (format "kubed-%S-entries" plrl-var)) + buff-fun (intern (format "kubed-%S-buffer" plrl-var)) + frmt-fun (intern (format "kubed-%S-format" plrl-var)) + affx-fun (intern (format "kubed-%S-affixation" plrl-var)) + updt-cmd (intern (format "kubed-update-%S" plrl-var)) + list-cmd (intern (format "kubed-list-%S" plrl-var)) + expl-cmd (intern (format "kubed-explain-%S" plrl-var)) + mark-cmd (intern (format "kubed-%S-mark-for-deletion" plrl-var)) + umrk-cmd (intern (format "kubed-%S-unmark" plrl-var)) + exec-cmd (intern (format "kubed-%S-execute" plrl-var)) + list-buf (format "*kubed-%S*" plrl-var) + out-name (format " *kubed-get-%S*" plrl-var) + err-name (format " *kubed-get-%S-stderr*" plrl-var) + dlt-errb (format " *kubed-%S-execute-stderr*" plrl-var) + dlt-name (intern (format "kubed-delete-%S" plrl-var)) + mod-name (intern (format "kubed-%S-mode" plrl-var))) + + ;; Extend `commands' with standard commands. + (dolist (c `((get "RET" "Switch to buffer showing description of" + (switch-to-buffer + ,(if namespaced + `(,desc-fun ,resource k8sns) + `(,desc-fun ,resource)))) + (get-in-other-window + "o" "Pop to buffer showing description of" + (switch-to-buffer-other-window + ,(if namespaced + `(,desc-fun ,resource k8sns) + `(,desc-fun ,resource)))) + (display "C-o" "Display description of" + (display-buffer + ,(if namespaced + `(,desc-fun ,resource k8sns) + `(,desc-fun ,resource)))) + (edit "e" "Edit" + ,(if namespaced + `(,edt-name ,resource k8sns) + `(,edt-name ,resource))) + (delete "D" "Delete" + ,(if namespaced + `(if k8sns + (when (y-or-n-p + (format ,(concat "Delete Kubernetes " + (symbol-name resource) + " `%s' in namespace `%s'?") + ,resource k8sns)) + (,dlt-name (list (list ,resource k8sns)))) + (when (y-or-n-p + (format ,(concat "Delete Kubernetes " + (symbol-name resource) + " `%s'?") + ,resource)) + (,dlt-name (list ,resource)))) + `(when (y-or-n-p + (format ,(concat "Delete Kubernetes " + (symbol-name resource) + " `%s'?") + ,resource)) + (,dlt-name (list ,resource))))))) + (push c commands)) + + ;; Generate code. + `(progn + (defvar ,hist-var nil + ,(format "History list for `%S'." read-fun)) + (defvar ,list-var nil + ,(format "List of Kubernetes resources of type `%S'." resource)) + (defvar ,hook-var nil + ,(format "List of functions to run after updating `%S'." list-var)) + (defvar ,proc-var nil + ,(format "Process that updates Kubernetes resources of type `%S'." resource)) + + (defun ,sure-fun () + ,(format "Populate `%S', if not already populated." list-var) + (unless (or ,list-var (process-live-p ,proc-var)) (,updt-cmd))) + + (defun ,updt-cmd () + ,(format "Update `%S'." list-var) + (interactive) + (when (process-live-p ,proc-var) (delete-process ,proc-var)) + (with-current-buffer (get-buffer-create ,out-name) + (erase-buffer)) + (setq ,proc-var + (make-process + :name ,(format "*kubed-get-%S*" plrl-var) + :buffer ,out-name + :stderr ,err-name + :command (list + kubed-kubectl-program + "get" ,(format "%S" plrl-var) + ,@(when namespaced + `((concat "--all-namespaces=" + (if kubed-all-namespaces-mode "true" "false")))) + (format "--output=custom-columns=%s" + (string-join + (cons "NAME:.metadata.name" + ,(if namespaced + `(append + (when kubed-all-namespaces-mode + '("NAMESPACE:.metadata.namespace")) + ',(mapcar (lambda (p) + (concat (upcase (symbol-name (car p))) + ":" + (cadr p))) + properties)) + `',(mapcar (lambda (p) + (concat (upcase (symbol-name (car p))) + ":" + (cadr p))) + properties))) + ","))) + :sentinel + (lambda (_proc status) + (cond + ((string= status "finished\n") + (let (new offsets eol) + (with-current-buffer ,out-name + (goto-char (point-min)) + (setq eol (pos-eol)) + (while (re-search-forward "[^ ]+" eol t) + (push (1- (match-beginning 0)) offsets)) + (setq offsets (nreverse offsets)) + (forward-char 1) + (while (not (eobp)) + (let ((cols nil) + (beg (car offsets)) + (ends (append (cdr offsets) (list (- (pos-eol) (point)))))) + ,@(let ((read-col + (lambda (p) + ;; Fresh list to avoid circles. + (list `(push ,(if-let ((f (nth 4 p))) + `(funcall ,f (string-trim (buffer-substring + (+ (point) beg) + (+ (point) (car ends))))) + `(string-trim (buffer-substring + (+ (point) beg) + (+ (point) (car ends))))) + cols) + '(setq beg (pop ends)))))) + (if namespaced + ;; Resource is namespaced, generate + ;; code that is sensitive to + ;; `kubed-all-namespaces-mode'. + `((if kubed-all-namespaces-mode + (progn + ,@(mapcan + read-col + ;; Two nils, one for the + ;; name column, another + ;; for the namespace. + `(nil nil . ,properties))) + ,@(mapcan read-col `(nil . ,properties)))) + ;; Non-namespaced. + (mapcan read-col `(nil . ,properties)))) + (push (nreverse cols) new)) + (forward-line 1))) + (setq ,list-var new + ,proc-var nil) + (run-hooks ',hook-var) + (message ,(format "Updated Kubernetes %S." plrl-var)))) + ((string= status "exited abnormally with code 1\n") + (with-current-buffer ,err-name + (goto-char (point-max)) + (insert "\n" status)) + (display-buffer ,err-name)))))) + (minibuffer-message ,(format "Updating Kubernetes %S..." plrl-var))) + + (defun ,affx-fun (,plrl-var) + ,(format "Return Kubernetes %s with completion affixations." + (upcase (symbol-name plrl-var))) + (let ((max (seq-max (cons 0 (mapcar #'string-width ,plrl-var))))) + (mapcar (lambda (,resource) + (list ,resource "" + (concat (make-string (1+ (- max (string-width ,resource))) ?\s) + (propertize (or (cadr (assoc ,resource ,list-var)) "") + 'face 'completions-annotations)))) + ,plrl-var))) + + (defun ,read-fun (prompt &optional default multi) + ,(format "Prompt with PROMPT for a Kubernetes %S name. + +Optional argument DEFAULT is the minibuffer default argument. + +Non-nil optional argument MULTI says to read and return a list +of %S, instead of just one." resource plrl-var) + (minibuffer-with-setup-hook + #',sure-fun + (funcall + (if multi #'completing-read-multiple #'completing-read) + (format-prompt prompt default) + (lambda (s p a) + (if (eq a 'metadata) + '(metadata + (category . ,(intern (format "kubernetes-%S" resource))) + ,@(when properties + `((affixation-function . ,affx-fun)))) + (while (and (process-live-p ,proc-var) + (null ,list-var)) + (accept-process-output ,proc-var 1)) + (complete-with-action a ,list-var s p))) + nil 'confirm nil ',hist-var default)) ) + + (defun ,read-crm (prompt &optional default) + ,(format "Prompt with PROMPT for Kubernetes %S names. + +Optional argument DEFAULT is the minibuffer default argument." resource) + (,read-fun prompt default t)) + + (defun ,desc-fun (,resource . ,(when namespaced '(&optional k8sns))) + ,(format "Return buffer describing Kubernetes %S %s" + resource (upcase (symbol-name resource))) + (let* ((buf (get-buffer-create ,buf-name)) + (fun (lambda (&optional _ _) + (let ((inhibit-read-only t) + (target (current-buffer))) + (buffer-disable-undo) + (with-temp-buffer + (unless (zerop + (call-process + kubed-kubectl-program nil t nil "get" + ,(symbol-name resource) "--output=yaml" ,resource + . ,(when namespaced + '((if k8sns + (concat "--namespace=" k8sns) + "--all-namespaces=false"))))) + (error ,(format "`kubectl get %S' failed" resource))) + (let ((source (current-buffer))) + (with-current-buffer target + (replace-buffer-contents source) + (set-buffer-modified-p nil) + (buffer-enable-undo)))))))) + (with-current-buffer buf + (funcall fun) + (goto-char (point-min)) + (run-hooks 'kubed-yaml-setup-hook) + (setq-local revert-buffer-function fun)) + buf)) + + ,(when namespaced + `(defun ,read-nms (prompt &optional default multi) + (let* ((choice + (funcall + (if multi #'completing-read-multiple #'completing-read) + (format-prompt prompt default) + (lambda (s p a) + (if (eq a 'metadata) + '(metadata + (category + . ,(intern (format "kubernetes-namespaced-%S" resource)))) + (while (and (process-live-p ,proc-var) + (null ,list-var)) + (accept-process-output ,proc-var 1)) + (complete-with-action a (mapcar (pcase-lambda (`(,name ,space . ,_)) + (concat name " " space)) + ,list-var) + s p))) + nil 'confirm nil ',hist-var default)) + (split (mapcar (lambda (c) + (split-string c " ")) + (ensure-list choice)))) + (if multi split (car split))))) + + (defun ,dsp-name (,resource . ,(when namespaced '(&optional k8sns))) + ,(format "Display Kubernetes %S %s." + resource (upcase (symbol-name resource))) + (interactive ,(if namespaced + `(if kubed-all-namespaces-mode + (,read-nms "Display") + (list (,read-fun "Display"))) + `(list (,read-fun "Display")))) + (display-buffer (,desc-fun ,resource . ,(when namespaced '(k8sns))))) + + (add-hook 'kubed-update-hook #',updt-cmd) + + ,(when namespaced + `(add-hook 'kubed-all-namespaces-mode-hook + (lambda () + (setq ,list-var nil) + (,updt-cmd)))) + + (defun ,edt-name (,resource . ,(when namespaced '(&optional k8sns))) + ,(format "Edit Kubernetes %S %s." + resource (upcase (symbol-name resource))) + (interactive ,(if namespaced + `(if kubed-all-namespaces-mode + (,read-nms "Edit") + (list (,read-fun "Edit"))) + `(list (,read-fun "Edit")))) + (unless (bound-and-true-p server-process) (server-start)) + (let ((process-environment + (cons (concat "KUBE_EDITOR=" emacsclient-program-name) + process-environment))) + (start-process ,(format "*kubed-%S-edit*" plrl-var) nil + kubed-kubectl-program "edit" + ,(symbol-name resource) ,resource + . ,(when namespaced + `((if k8sns + (concat "--namespace=" k8sns) + "-o=yaml")))))) + + ,(if namespaced + `(defun ,dlt-name (,plrl-var) + ,(format "Delete Kubernetes %S %s." + plrl-var (upcase (symbol-name plrl-var))) + (interactive (if kubed-all-namespaces-mode + (,read-nms "Delete" nil t) + (list (,read-crm "Delete")))) + (unless ,plrl-var + (user-error ,(format "You didn't specify %S to delete" plrl-var))) + (if kubed-all-namespaces-mode + (pcase-dolist (`(,name ,space) ,plrl-var) + (message ,(concat "Deleting Kubernetes " + (symbol-name resource) + " `%s' in namespace `%s'...") + name space) + (if (zerop (call-process + kubed-kubectl-program nil nil nil + "delete" "--namespace" space + ,(symbol-name plrl-var) name)) + (message ,(concat "Deleting Kubernetes " + (symbol-name resource) + " `%s' in namespace `%s'... Done.") + name space) + (error ,(concat "Failed to delete Kubernetes" + (symbol-name resource) + " `%s' in namespace `%s'") + name space))) + (message ,(concat "Deleting Kubernetes " + (symbol-name plrl-var) + " `%s'...") + (string-join ,plrl-var "', `")) + (if (zerop (apply #'call-process + kubed-kubectl-program nil nil nil + "delete" ,(symbol-name plrl-var) ,plrl-var)) + (message ,(concat "Deleting Kubernetes " + (symbol-name plrl-var) + " `%s'... Done.") + (string-join ,plrl-var "', `")) + (error ,(concat "Failed to delete Kubernetes" + (symbol-name plrl-var) + " `%s'") + (string-join ,plrl-var "', `"))))) + `(defun ,dlt-name (,plrl-var) + ,(format "Delete Kubernetes %S %s." plrl-var + (upcase (symbol-name plrl-var))) + (interactive (list (,read-crm "Delete"))) + (unless ,plrl-var + (user-error ,(format "You didn't specify %S to delete" plrl-var))) + (message ,(concat "Deleting Kubernetes " + (symbol-name plrl-var) + " `%s'...") + (string-join ,plrl-var "', `")) + (if (zerop (apply #'call-process + kubed-kubectl-program nil nil nil + "delete" ,(symbol-name plrl-var) ,plrl-var)) + (message ,(concat "Deleting Kubernetes " + (symbol-name plrl-var) + " `%s'... Done.") + (string-join ,plrl-var "', `")) + (error ,(concat "Failed to delete Kubernetes" + (symbol-name plrl-var) + " `%s'") + (string-join ,plrl-var "', `"))))) + + (defvar-local ,ents-var nil) + + (defun ,ents-fun () + ,(format "`tabulated-list-entries' function for `%s'." mod-name) + (mapcar + (lambda (c) (list ,(if namespaced + `(if kubed-all-namespaces-mode + (concat (car c) " " (cadr c)) + (car c)) + `(car c)) + (apply #'vector c))) + ,ents-var)) + + (defun ,mark-cmd () + ,(format "Mark Kubernetes %S at point for deletion." resource) + (interactive "" ,mod-name) + (tabulated-list-put-tag + (propertize "D" 'help-echo "Marked for deletion") t)) + + (defun ,umrk-cmd () + ,(format "Remove mark from Kubernetes %S at point." resource) + (interactive "" ,mod-name) + (tabulated-list-put-tag " " t)) + + (defun ,exec-cmd () + ,(format "Delete marked Kubernetes %S." plrl-var) + (interactive "" ,mod-name) + (let (delete-list) + (save-excursion + (goto-char (point-min)) + (while (not (eobp)) + (when (eq (char-after) ?D) + (push (tabulated-list-get-id) delete-list)) + (forward-line))) + (if delete-list + (when (y-or-n-p (format ,(concat "Delete %d marked Kubernetes " + (symbol-name plrl-var) "?") + (length delete-list))) + ,@(if namespaced + `((if kubed-all-namespaces-mode + (save-excursion + (goto-char (point-min)) + (while (not (eobp)) + (when (member (tabulated-list-get-id) delete-list) + (tabulated-list-put-tag + (propertize "K" 'help-echo "Deletion in progress")) + (let* ((k8sent (tabulated-list-get-entry)) + (name (aref k8sent 0)) + (space (aref k8sent 1))) + (make-process + :name ,(format "*kubed-%S-execute*" plrl-var) + :stderr ,dlt-errb + :command (list kubed-kubectl-program + "delete" + "--namespace" space + ,(symbol-name plrl-var) + name) + :sentinel (lambda (_proc status) + (cond + ((string= status "finished\n") + (message (format ,(concat "Deleted Kubernetes " + (symbol-name resource) + " `%s' in namespace `%s'.") + name space)) + (,updt-cmd)) + ((string= status "exited abnormally with code 1\n") + (with-current-buffer ,dlt-errb + (goto-char (point-max)) + (insert "\n" status)) + (display-buffer ,dlt-errb))))))) + (forward-line))) + (save-excursion + (goto-char (point-min)) + (while (not (eobp)) + (when (member (tabulated-list-get-id) delete-list) + (tabulated-list-put-tag + (propertize "K" 'help-echo "Deletion in progress"))) + (forward-line))) + (make-process + :name ,(format "*kubed-%S-execute*" plrl-var) + :stderr ,dlt-errb + :command (append + (list kubed-kubectl-program + "delete" ,(symbol-name plrl-var)) + delete-list) + :sentinel (lambda (_proc status) + (cond + ((string= status "finished\n") + (message (format ,(concat "Deleted %d marked Kubernetes " + (symbol-name plrl-var) ".") + (length delete-list))) + (,updt-cmd)) + ((string= status "exited abnormally with code 1\n") + (with-current-buffer ,dlt-errb + (goto-char (point-max)) + (insert "\n" status)) + (display-buffer ,dlt-errb))))))) + `((save-excursion + (goto-char (point-min)) + (while (not (eobp)) + (when (member (tabulated-list-get-id) delete-list) + (tabulated-list-put-tag + (propertize "K" 'help-echo "Deletion in progress"))) + (forward-line))) + (make-process + :name ,(format "*kubed-%S-execute*" plrl-var) + :stderr ,dlt-errb + :command (append + (list kubed-kubectl-program + "delete" ,(symbol-name plrl-var)) + delete-list) + :sentinel (lambda (_proc status) + (cond + ((string= status "finished\n") + (message (format ,(concat "Deleted %d marked Kubernetes " + (symbol-name plrl-var) ".") + (length delete-list))) + (,updt-cmd)) + ((string= status "exited abnormally with code 1\n") + (with-current-buffer ,dlt-errb + (goto-char (point-max)) + (insert "\n" status)) + (display-buffer ,dlt-errb)))))))) + (user-error ,(format "No Kubernetes %S marked for deletion" plrl-var))))) + + ,(if crt-spec `(defun ,crt-name . ,crt-spec) + `(defun ,crt-name (definition) + ,(format "Create Kubernetes %s from definition file DEFINITION." + (symbol-name resource)) + (interactive (list (kubed-read-resource-definition-file-name + ,(symbol-name resource)))) + (kubed-create definition ,(symbol-name resource)))) + + ,@(mapcar + (pcase-lambda (`(,suffix ,_key ,desc . ,body)) + `(defun ,(intern (format "kubed-%S-%S" plrl-var suffix)) () + ,(format "%s Kubernetes %S at point." desc resource) + (interactive "" ,mod-name) + (if-let ,(if namespaced + `((k8sent (tabulated-list-get-entry)) + (,resource (aref k8sent 0))) + `(,resource (tabulated-list-get-id))) + ,(if namespaced + `(let ((k8sns (when kubed-all-namespaces-mode + (aref (tabulated-list-get-entry) 1)))) + ,@body) + `(progn ,@body)) + (user-error ,(format "No Kubernetes %S at point" resource))))) + commands) + + (defvar-keymap ,(intern (format "kubed-%S-mode-map" plrl-var)) + :doc ,(format "Keymap for `%S" mod-name) + "A" #'kubed-all-namespaces-mode + "G" #',updt-cmd + "d" #',mark-cmd + "x" #',exec-cmd + "u" #',umrk-cmd + "+" #',crt-name + ,@(mapcan + (pcase-lambda (`(,suffix ,key ,_desc . ,_body)) + (when key + (list key `#',(intern (format "kubed-%S-%S" plrl-var suffix))))) + commands)) + + (defvar ,frmt-var + ',(let ((i 0) + (res nil)) + (dolist (p properties) + (setq i (1+ i)) + (push + (append + (list (capitalize (symbol-name (car p))) + (caddr p) + (if-let ((sorter (cadddr p))) + `(lambda (l r) + ,(if namespaced + `(let ((c (+ ,i (if kubed-all-namespaces-mode 1 0)))) + (funcall ,sorter (aref (cadr l) c) (aref (cadr r) c))) + `(funcall ,sorter (aref (cadr l) ,i) (aref (cadr r) ,i)))) + t)) + (nthcdr 5 p)) + res)) + (reverse res))) + + (defun ,frmt-fun () + (apply #'vector + (cons + kubed-name-column + ,(if namespaced + `(append + (when kubed-all-namespaces-mode + (list kubed-namespace-column)) + ,frmt-var) + frmt-var)))) + + (define-derived-mode ,mod-name tabulated-list-mode + (list ,(format "Kubernetes %ss" (capitalize (symbol-name resource))) + (list ',proc-var + (list :propertize "[...]" 'help-echo "Updating..."))) + ,(format "Major mode for listing Kubernetes %S." plrl-var) + :interactive nil + (add-hook 'revert-buffer-restore-functions + (lambda () + (let (marks) + (save-excursion + (goto-char (point-min)) + (while (not (eobp)) + (unless (eq (char-after) ?\s) + (push (cons (tabulated-list-get-id) + ;; Preserve mark properties. + (buffer-substring (point) (1+ (point)))) + marks)) + (forward-line))) + (lambda () + (save-excursion + (goto-char (point-min)) + (while (not (eobp)) + (when-let ((mark (alist-get (tabulated-list-get-id) marks nil nil #'equal))) + (tabulated-list-put-tag mark)) + (forward-line)))))) + nil t) + (setq tabulated-list-format (,frmt-fun)) + (setq tabulated-list-entries #',ents-fun) + (setq tabulated-list-padding 2) + (setq-local truncate-string-ellipsis (propertize ">" 'face 'shadow)) + (tabulated-list-init-header)) + + (defun ,buff-fun (,plrl-var &optional buffer frozen) + (with-current-buffer (or buffer (get-buffer-create ,list-buf)) + (,mod-name) + (let* ((buf (current-buffer)) + (fun (lambda () + (when (buffer-live-p buf) + (with-current-buffer buf + (unless kubed-frozen + (setq ,ents-var ,list-var) + (setq tabulated-list-format (,frmt-fun)) + (tabulated-list-init-header) + (revert-buffer))))))) + (add-hook ',hook-var fun) + (add-hook 'kill-buffer-hook + (lambda () (remove-hook ',hook-var fun)) + nil t)) + (setq kubed-frozen frozen) + (setq ,ents-var ,plrl-var) + (tabulated-list-print) + (current-buffer))) + + (defun ,list-cmd () + ,(format "List Kubernetes %S." plrl-var) + (interactive) + (,sure-fun) + (pop-to-buffer (,buff-fun ,list-var))) + + (defun ,expl-cmd () + ,(format "Show help buffer with explanation about Kubernetes %S." plrl-var) + (interactive) + (kubed-explain ,(symbol-name plrl-var))) + + (defvar-keymap ,map-name + :doc ,(format "Prefix keymap for Kubed %s commands." + (symbol-name resource)) + :prefix ',map-name + "l" #',list-cmd + "c" #',crt-name + "e" #',edt-name + "d" #',dlt-name + "g" #',dsp-name + "u" #',updt-cmd + "E" #',expl-cmd + ,@prf-keys)))) + +(defvar tramp-kubernetes-namespace) + +;;;###autoload (autoload 'kubed-display-pod "kubed" nil t) +;;;###autoload (autoload 'kubed-edit-pod "kubed" nil t) +;;;###autoload (autoload 'kubed-delete-pods "kubed" nil t) +;;;###autoload (autoload 'kubed-list-pods "kubed" nil t) +;;;###autoload (autoload 'kubed-create-pod "kubed" nil t) +;;;###autoload (autoload 'kubed-pod-prefix-map "kubed" nil t 'keymap) +(kubed-define-resource pod + ((phase ".status.phase" 10 + nil ; sorting function + (lambda (ph) + (if-let ((face (pcase ph + ;; TODO: Define/derive bespoke faces. + ("Pending" 'warning) + ("Running" 'success) + ("Succeeded" 'shadow) + ("Failed" 'error)))) + (propertize ph 'face face) + ph))) + (ready ".status.containerStatuses[?(.ready==true)].name" 6 + (lambda (l r) (< (string-to-number l) (string-to-number r))) + (lambda (cs) + (if (string= cs "") "0" + (number-to-string (1+ (seq-count (lambda (c) (= c ?,)) cs))))) + :right-align t) + (total ".status.containerStatuses[*].name" 6 + (lambda (l r) (< (string-to-number l) (string-to-number r))) + (lambda (cs) + (if (string= cs "") "0" + (number-to-string (1+ (seq-count (lambda (c) (= c ?,)) cs))))) + :right-align t) + (starttime ".status.startTime" 20)) + :prefix ("L" #'kubed-logs + "A" #'kubed-attach + "X" #'kubed-exec + "F" #'kubed-forward-port-to-pod) + (dired "C-d" "Start Dired in home directory of first container of" + (let ((ns (when k8sns (concat "%" k8sns)))) + (dired (concat "/kubernetes:" pod ns ":")))) + (shell "s" "Start shell in home directory of first container of" + (let* ((ns (when k8sns (concat "%" k8sns))) + (default-directory (concat "/kubernetes:" pod ns ":"))) + (shell (format "*kubed-pod-%s-shell*" pod)))) + (attach "a" "Attach to remote process running on" + (kubed-attach pod (kubed-read-container pod "Container" t k8sns) + k8sns t t)) + (exec "X" "Execute command in" + (let ((container (kubed-read-container pod "Container" t k8sns)) + (cmd-args (split-string-and-unquote + (read-string "Execute command: ")))) + (kubed-exec pod (car cmd-args) container k8sns t t (cdr cmd-args)))) + (logs "l" "Show logs for a container of" + (kubed-logs pod (kubed-read-container pod "Container" t k8sns))) + (forward-port "F" "Forward local network port to remote port of" + (let ((local-port (read-number "Forward local port: "))) + (kubed-forward-port-to-pod + pod local-port + (read-number (format "Forward local port %d to remote port: " + local-port)) + k8sns)))) + +;;;###autoload (autoload 'kubed-display-namespace "kubed" nil t) +;;;###autoload (autoload 'kubed-edit-namespace "kubed" nil t) +;;;###autoload (autoload 'kubed-delete-namespaces "kubed" nil t) +;;;###autoload (autoload 'kubed-list-namespaces "kubed" nil t) +;;;###autoload (autoload 'kubed-create-namespace "kubed" nil t) +;;;###autoload (autoload 'kubed-namespace-prefix-map "kubed" nil t 'keymap) +(kubed-define-resource namespace + ((phase ".status.phase" 10 + nil ; sorting function + (lambda (ph) + (if-let ((face (pcase ph + ;; TODO: Define/derive bespoke faces. + ("Active" 'success) + ("Terminating" 'shadow)))) + (propertize ph 'face face) + ph))) + (creationtimestamp ".metadata.creationTimestamp" 20)) + :namespaced nil + :prefix ("S" #'kubed-set-namespace) + :create + ((name) "Create Kubernetes namespace with name NAME." + (interactive (list (read-string "Create namespace with name: "))) + (unless (zerop + (call-process + kubed-kubectl-program nil nil nil + "create" "namespace" name)) + (user-error "Failed to create Kubernetes namespace with name `%s'" name)) + (message "Created Kubernetes namespace with name `%s'." name) + (kubed-update-namespaces)) + (set "s" "Set current namespace to" + (save-excursion + (goto-char (point-min)) + (while (not (eobp)) + (when (eq (char-after) ?*) + (tabulated-list-put-tag " ")) + (forward-line))) + (kubed-set-namespace namespace) + (tabulated-list-put-tag + (propertize "*" 'help-echo "Current namespace")))) + +;;;###autoload (autoload 'kubed-display-persistentvolume "kubed" nil t) +;;;###autoload (autoload 'kubed-edit-persistentvolume "kubed" nil t) +;;;###autoload (autoload 'kubed-delete-persistentvolumes "kubed" nil t) +;;;###autoload (autoload 'kubed-list-persistentvolumes "kubed" nil t) +;;;###autoload (autoload 'kubed-create-persistentvolume "kubed" nil t) +;;;###autoload (autoload 'kubed-persistentvolume-prefix-map "kubed" nil t 'keymap) +(kubed-define-resource persistentvolume () :namespaced nil) + +;;;###autoload (autoload 'kubed-display-service "kubed" nil t) +;;;###autoload (autoload 'kubed-edit-service "kubed" nil t) +;;;###autoload (autoload 'kubed-delete-services "kubed" nil t) +;;;###autoload (autoload 'kubed-list-services "kubed" nil t) +;;;###autoload (autoload 'kubed-create-service "kubed" nil t) +;;;###autoload (autoload 'kubed-service-prefix-map "kubed" nil t 'keymap) +(kubed-define-resource service) + +;;;###autoload (autoload 'kubed-display-secret "kubed" nil t) +;;;###autoload (autoload 'kubed-edit-secret "kubed" nil t) +;;;###autoload (autoload 'kubed-delete-secrets "kubed" nil t) +;;;###autoload (autoload 'kubed-list-secrets "kubed" nil t) +;;;###autoload (autoload 'kubed-create-secret "kubed" nil t) +;;;###autoload (autoload 'kubed-secret-prefix-map "kubed" nil t 'keymap) +(kubed-define-resource secret + ((type ".type" 32) (creationtimestamp ".metadata.creationTimestamp" 20))) + +;;;###autoload (autoload 'kubed-display-job "kubed" nil t) +;;;###autoload (autoload 'kubed-edit-job "kubed" nil t) +;;;###autoload (autoload 'kubed-delete-jobs "kubed" nil t) +;;;###autoload (autoload 'kubed-list-jobs "kubed" nil t) +;;;###autoload (autoload 'kubed-create-job "kubed" nil t) +;;;###autoload (autoload 'kubed-job-prefix-map "kubed" nil t 'keymap) +(kubed-define-resource job + ((status ".status.conditions[0].type" 10) (starttime ".status.startTime" 20))) + +;;;###autoload (autoload 'kubed-display-deployment "kubed" nil t) +;;;###autoload (autoload 'kubed-edit-deployment "kubed" nil t) +;;;###autoload (autoload 'kubed-delete-deployments "kubed" nil t) +;;;###autoload (autoload 'kubed-list-deployments "kubed" nil t) +;;;###autoload (autoload 'kubed-create-deployment "kubed" nil t) +;;;###autoload (autoload 'kubed-deployment-prefix-map "kubed" nil t 'keymap) +(kubed-define-resource deployment + ((reps ".status.replicas" 4 + (lambda (l r) (< (string-to-number l) (string-to-number r))) + nil ; formatting function + :right-align t) + (creationtimestamp ".metadata.creationTimestamp" 20)) + :create + ((name images &optional namespace replicas port command) + "Deploy IMAGES to Kubernetes in deployment with name NAME. + +Optional argument NAMESPACE is the namespace to use for the deployment, +defaulting to the current namespace, REPLICAS in the number of replicas +to create for each image, PORT is the port to expose, and COMMAND is an +optional command to run in the images." + (interactive + (let ((name (read-string "Create deployment with name: ")) + (images nil) + (replicas (prefix-numeric-value current-prefix-arg)) + (port nil) + (command nil) + (namespace nil)) + (dolist (arg (kubed-transient-args 'kubed-transient-create-deployment)) + (cond + ((string-match "--replicas=\\(.+\\)" arg) + (setq replicas (string-to-number (match-string 1 arg)))) + ((string-match "--image=\\(.+\\)" arg) + (push (match-string 1 arg) images)) + ((string-match "--port=\\(.+\\)" arg) + (setq port (string-to-number (match-string 1 arg)))) + ((string-match "--namespace=\\(.+\\)" arg) + (setq namespace (match-string 1 arg))) + ((string-match "-- =\\(.+\\)" arg) + (setq command (split-string-and-unquote (match-string 1 arg)))))) + (unless images + (setq images (kubed-read-container-images "Images to deploy"))) + (list name images namespace replicas port command))) + (unless (zerop + (apply #'call-process + kubed-kubectl-program nil nil nil + "create" "deployment" name + (append + (mapcar (lambda (image) (concat "--image=" image)) images) + (when namespace (list (concat "--namespace=" namespace))) + (when replicas (list (format "--replicas=%d" replicas))) + (when port (list (format "--port=%d" port))) + (when command (cons "--" command))))) + (user-error "Failed to create Kubernetes deployment `%s'" name)) + (message "Created Kubernetes deployment `%s'." name) + (kubed-update-deployments))) + +;;;###autoload (autoload 'kubed-display-replicaset "kubed" nil t) +;;;###autoload (autoload 'kubed-edit-replicaset "kubed" nil t) +;;;###autoload (autoload 'kubed-delete-replicasets "kubed" nil t) +;;;###autoload (autoload 'kubed-list-replicasets "kubed" nil t) +;;;###autoload (autoload 'kubed-create-replicaset "kubed" nil t) +;;;###autoload (autoload 'kubed-replicaset-prefix-map "kubed" nil t 'keymap) +(kubed-define-resource replicaset + ((reps ".status.replicas" 4 + (lambda (l r) (< (string-to-number l) (string-to-number r))) + nil ; formatting function + :right-align t) + (ownerkind ".metadata.ownerReferences[0].kind" 12) + (ownername ".metadata.ownerReferences[0].name" 16) + (creationtimestamp ".metadata.creationTimestamp" 20))) + +;;;###autoload (autoload 'kubed-display-statefulset "kubed" nil t) +;;;###autoload (autoload 'kubed-edit-statefulset "kubed" nil t) +;;;###autoload (autoload 'kubed-delete-statefulsets "kubed" nil t) +;;;###autoload (autoload 'kubed-list-statefulsets "kubed" nil t) +;;;###autoload (autoload 'kubed-create-statefulset "kubed" nil t) +;;;###autoload (autoload 'kubed-statefulset-prefix-map "kubed" nil t 'keymap) +(kubed-define-resource statefulset + ((reps ".status.replicas" 4 + (lambda (l r) (< (string-to-number l) (string-to-number r))) + nil ; formatting function + :right-align t) + (ownerkind ".metadata.ownerReferences[0].kind" 12) + (ownername ".metadata.ownerReferences[0].name" 16) + (creationtimestamp ".metadata.creationTimestamp" 20))) + +;;;###autoload (autoload 'kubed-display-cronjob "kubed" nil t) +;;;###autoload (autoload 'kubed-edit-cronjob "kubed" nil t) +;;;###autoload (autoload 'kubed-delete-cronjobs "kubed" nil t) +;;;###autoload (autoload 'kubed-list-cronjobs "kubed" nil t) +;;;###autoload (autoload 'kubed-create-cronjob "kubed" nil t) +;;;###autoload (autoload 'kubed-cronjob-prefix-map "kubed" nil t 'keymap) +(kubed-define-resource cronjob + ((schedule ".spec.schedule" 20) + (suspend ".spec.suspend" 20) + (lastschedule ".status.lastScheduleTime" 20) + (lastsuccess ".status.lastSuccessfulTime" 20) + (activejob ".status.active[0].name" 36)) + :create + ((name image schedule &optional namespace command) + "Schedule IMAGE to run in a cronjob with name NAME according to SCHEDULE. + +Optional argument NAMESPACE is the namespace to use for the cronjob, +defaulting to the current namespace. COMMAND is a list of strings that +represent a program followed by its arguments, if it non-nil then it +overrides the default command IMAGE runs." + (interactive + (let ((name (read-string "Create cronjob with name: ")) + (image nil) + (schedule nil) + (command nil) + (namespace nil)) + (dolist (arg (kubed-transient-args 'kubed-transient-create-cronjob)) + (cond + ((string-match "--image=\\(.+\\)" arg) + (setq image (match-string 1 arg))) + ((string-match "--schedule=\\(.+\\)" arg) + (setq schedule (match-string 1 arg))) + ((string-match "--namespace=\\(.+\\)" arg) + (setq namespace (match-string 1 arg))) + ((string-match "-- =\\(.+\\)" arg) + (setq command (split-string-and-unquote (match-string 1 arg)))))) + (unless image + (setq image (read-string "Image to run: " nil 'kubed-container-images-history))) + (unless schedule + (setq schedule (read-string "Cron schedule: " "* * * * *"))) + (list name image schedule namespace command))) + (unless (zerop + (apply #'call-process + kubed-kubectl-program nil nil nil + "create" "cronjob" name + "--image" image "--schedule" schedule + (append + (when namespace (list "--namespace" namespace)) + (when command (cons "--" command))))) + (user-error "Failed to create Kubernetes cronjob `%s'" name)) + (message "Created Kubernetes cronjob `%s'." name) + (kubed-update-cronjobs))) + +;;;###autoload (autoload 'kubed-display-ingressclass "kubed" nil t) +;;;###autoload (autoload 'kubed-edit-ingressclass "kubed" nil t) +;;;###autoload (autoload 'kubed-delete-ingressclasses "kubed" nil t) +;;;###autoload (autoload 'kubed-list-ingressclasss "kubed" nil t) +;;;###autoload (autoload 'kubed-create-ingressclass "kubed" nil t) +;;;###autoload (autoload 'kubed-ingressclass-prefix-map "kubed" nil t 'keymap) +(kubed-define-resource ingressclass + ((controller ".spec.controller" 32) + (creationtimestamp ".metadata.creationTimestamp" 20)) + :namespaced nil + :plural ingressclasses) + +;;;###autoload (autoload 'kubed-display-ingress "kubed" nil t) +;;;###autoload (autoload 'kubed-edit-ingress "kubed" nil t) +;;;###autoload (autoload 'kubed-delete-ingresses "kubed" nil t) +;;;###autoload (autoload 'kubed-list-ingresss "kubed" nil t) +;;;###autoload (autoload 'kubed-create-ingress "kubed" nil t) +;;;###autoload (autoload 'kubed-ingress-prefix-map "kubed" nil t 'keymap) +(kubed-define-resource ingress + ((class ".spec.ingressClassName" 8) + (creationtimestamp ".metadata.creationTimestamp" 20)) + :plural ingresses + :create + ((name rules &optional namespace class default-backend annotations) + "Create Kubernetes ingress with name NAME and rules RULES. + +Optional argument NAMESPACE is the namespace to use for the ingress, +defaulting to the current namespace. CLASS is the ingress class, +ANNOTATIONS are a list of annotations for the created ingress, and +DEFAULT-BACKEND is the service to use as a backend for unhandled URLs." + (interactive + (let ((name (read-string "Create ingress with name: ")) + (rules nil) + (namespace nil) + (class nil) + (annotations nil) + (default-backend nil)) + (dolist (arg (kubed-transient-args 'kubed-transient-create-ingress)) + (cond + ((string-match "--rule=\\(.+\\)" arg) + (push (match-string 1 arg) rules)) + ((string-match "--namespace=\\(.+\\)" arg) + (setq namespace (match-string 1 arg))) + ((string-match "--class=\\(.+\\)" arg) + (setq class (match-string 1 arg))) + ((string-match "--default-backend=\\(.+\\)" arg) + (setq default-backend (match-string 1 arg))) + ((string-match "--annotation=\\(.+\\)" arg) + (push (match-string 1 arg) annotations)))) + (unless rules (setq rules (kubed-read-ingress-rules))) + (list name rules namespace class default-backend annotations))) + (unless (zerop + (apply #'call-process + kubed-kubectl-program nil nil nil + "create" "ingress" name + (append + (mapcan (lambda (rule) (list "--rule" rule)) rules) + (when namespace (list "--namespace" namespace)) + (when class (list "--class" class)) + (when default-backend + (list "--default-backend" default-backend)) + (mapcan (lambda (ann) (list "--annotation" ann)) + annotations)))) + (user-error "Failed to create Kubernetes ingress `%s'" name)) + (message "Created Kubernetes ingress `%s'." name) + (kubed-update-ingresses))) + +;; TODO: Events may be numerous. Need to only get a few. +;; ;;;###autoload (autoload 'kubed-list-events "kubed" nil t) +;; ;;;###autoload (autoload 'kubed-event-prefix-map "kubed" nil t 'keymap) +;; (kubed-define-resource event +;; ((last ".lastTimestamp" 20) +;; (type ".type" 10) +;; (reason ".reason" 20) +;; (objectkind ".involvedObject.kind" 12) +;; (objectname ".involvedObject.name" 16) +;; (message ".message" 36))) + +(defun kubed-contexts () + "Return list of Kubernetes contexts." + (process-lines kubed-kubectl-program "config" "get-contexts" "-o" "name")) + +(defun kubed-current-context () + "Return current Kubernetes context." + (car (process-lines kubed-kubectl-program "config" "current-context"))) + +(defvar kubed-context-history nil + "History list for `kubed-read-context'.") + +(defun kubed-read-context (prompt &optional default) + "Prompt with PROMPT for a Kubernetes context. + +Optional argument DEFAULT is the minibuffer default argument." + (completing-read (format-prompt prompt default) + (kubed-contexts) + nil 'confirm nil 'kubed-context-history default)) + +;;;###autoload +(defun kubed-use-context (context) + "Set current Kubernetes context to CONTEXT." + (interactive + (list (kubed-read-context "Use context" (kubed-current-context)))) + (unless (zerop + (call-process + kubed-kubectl-program nil nil nil + "config" "use-context" context)) + (user-error "Failed to use Kubernetes context `%s'" context)) + (message "Now using Kubernetes context `%s'." context) + (kubed-update-all)) + +;;;###autoload +(defun kubed-rename-context (old new) + "Rename Kubernetes context OLD to NEW." + (interactive + (let ((old (kubed-read-context "Rename context" (kubed-current-context)))) + (list old (read-string (format-prompt "Rename context to" old) + nil 'kubed-context-history old)))) + (unless (zerop + (call-process + kubed-kubectl-program nil nil nil + "config" "rename-context" old new)) + (user-error "Failed to rename Kubernetes context `%s' to `%s'" old new)) + (message "Renamed Kubernetes context `%s' to `%s'." old new)) + +;;;###autoload +(defun kubed-display-config () + "Display current Kubernetes client settings in a YAML buffer." + (interactive) + (let* ((buf (get-buffer-create "*kubed-config*")) + (fun (lambda (&optional _ _) + (let ((inhibit-read-only t) + (target (current-buffer))) + (buffer-disable-undo) + (with-temp-buffer + (unless (zerop + (call-process + kubed-kubectl-program nil t nil "config" "view")) + (error "`kubectl config view'")) + (let ((source (current-buffer))) + (with-current-buffer target + (replace-buffer-contents source) + (set-buffer-modified-p nil) + (buffer-enable-undo)))))))) + (with-current-buffer buf + (funcall fun) + (goto-char (point-min)) + (run-hooks 'kubed-yaml-setup-hook) + (setq-local revert-buffer-function fun)) + (display-buffer buf))) + +(defun kubed-current-namespace (&optional context) + "Return current Kubernetes namespace for context CONTEXT." + (car (process-lines + kubed-kubectl-program + "config" "view" "-o" + (format "jsonpath={.contexts[?(.name==\"%s\")].context.namespace}" + (or context (kubed-current-context)))))) + +;;;###autoload +(defun kubed-set-namespace (ns) + "Set current Kubernetes namespace to NS." + (interactive + (list (kubed-read-namespace "Set namespace" (kubed-current-namespace)))) + (unless (zerop + (call-process + kubed-kubectl-program nil nil nil + "config" "set-context" "--current" "--namespace" ns)) + (user-error "Failed to set Kubernetes namespace to `%s'" ns)) + (message "Kubernetes namespace is now `%s'." ns) + (kubed-update-all)) + +(defcustom kubed-read-resource-definition-filter-files-by-kind t + "Whether to filter file completion candidates by their Kubernetes \"kind\". + +If this is non-nil, `kubed-read-resource-definition-file-name' only +suggests files with the right \"kind\" as completion candidates when you +call it with non-nil KIND argument. This is useful because you get more +relevant completions, but it may become slow in directories with many +large JSON and YAML files, in which case you can set this option to nil." + :type 'boolean) + +(defun kubed-read-resource-definition-file-name (&optional kind) + "Prompt for Kubernetes resource definition file name. + +Optional argument KIND is the kind of resource the file should define. +If `kubed-read-resource-definition-filter-files-by-kind' is non-nil, +this function suggests only files that define resources of kind KIND as +completion candidates." + (read-file-name + (format "%s definition file: " (or kind "Resource")) nil nil t nil + (if (and kind kubed-read-resource-definition-filter-files-by-kind + (executable-find "grep")) + (let ((cache (make-hash-table :test 'equal))) + (lambda (f) + (or (file-directory-p f) + (when-let ((ext (and (string-match "\\.[^.]*\\'" f) + (substring f (1+ (match-beginning 0)))))) + (or (and (member ext '("yaml" "yml")) + (pcase (gethash (expand-file-name f) cache 'noval) + ('noval + (puthash (expand-file-name f) + (zerop (call-process + "grep" f nil nil "-i" + (format "^kind: %s$" kind))) + cache)) + (val val))) + (and (equal ext "json") + (pcase (gethash (expand-file-name f) cache 'noval) + ('noval + (puthash (expand-file-name f) + (zerop (call-process + "grep" f nil nil "-i" + (format "^kind: %s$" kind))) + cache)) + (val val)))))))) + (lambda (f) + (or (file-directory-p f) + (when (string-match "\\.[^.]*\\'" f) + (member (substring f (1+ (match-beginning 0))) + '("yaml" "yml" "json")))))))) + +;;;###autoload +(defun kubed-apply (config &optional kind) + "Apply CONFIG to Kubernetes resource of kind KIND." + (interactive + (list (or (seq-some + (lambda (arg) + (when (string-match "--filename=\\(.+\\)" arg) + (match-string 1 arg))) + (kubed-transient-args 'kubed-transient-apply)) + (kubed-read-resource-definition-file-name)))) + (let ((kind (or kind "resource"))) + (message "Applying Kubernetes %s configuration `%s'..." kind config) + (call-process kubed-kubectl-program nil nil nil + "apply" "-f" (expand-file-name config)) + (message "Applying Kubernetes %s configuration `%s'... Done." kind config))) + +;;;###autoload +(defun kubed-create (definition &optional kind) + "Create Kubernetes resource of kind KIND with definition DEFINITION." + (interactive + (list (or (seq-some + (lambda (arg) + (when (string-match "--filename=\\(.+\\)" arg) + (match-string 1 arg))) + (kubed-transient-args 'kubed-transient-create)) + (kubed-read-resource-definition-file-name)))) + (let ((kind (or kind "resource"))) + (message "Creating Kubernetes %s with definition `%s'..." kind definition) + (message "Creating Kubernetes %s with definition `%s'... Done. New %s name is `%s'." + kind definition kind + (car (process-lines kubed-kubectl-program + "create" "-f" (expand-file-name definition) + "-o" "jsonpath={.metadata.name}"))))) + +;;;###autoload +(defun kubed-run + (pod image &optional namespace port attach stdin tty rm envs command args) + "Run IMAGE in Kubernetes POD. + +Optional argument NAMESPACE is the namespace to use for the created pod, +defaulting to the current namespace. PORT is the port to expose, +defaulting to none. If ATTACH is non-nil, then attach to the created +image with a `comint-mode' buffer, and pop to that buffer. Non-nil +STDIN says to keep the standard input of the container open; non-nil TTY +says to allocate a TTY for the container; and non-nil RM says to remove +the container after it exits. ENVS is a list of strings \"VAR=VAL\" +which specify environment variables VAR and values VAL to give them in +the created container. ARGS are command line arguments for the +container command. If COMMAND is non-nil, ARGS consist of a complete +command line, that overrides the container command instead of just +providing it with arguments." + (interactive + (let ((pod (read-string "Run image in pod with name: ")) + (image nil) (port nil) (namespace nil) (attach nil) (stdin nil) + (tty nil) (rm nil) (envs nil) (command nil) (args nil)) + (dolist (arg (kubed-transient-args 'kubed-transient-run)) + (cond + ((string-match "--image=\\(.+\\)" arg) + (setq image (match-string 1 arg))) + ((string-match "--port=\\(.+\\)" arg) + (setq port (string-to-number (match-string 1 arg)))) + ((string-match "--namespace=\\(.+\\)" arg) + (setq namespace (match-string 1 arg))) + ((equal "--attach" arg) (setq attach t)) + ((equal "--stdin" arg) (setq stdin t)) + ((equal "--tty" arg) (setq tty t)) + ((equal "--rm" arg) (setq rm t)) + ((equal "--command" arg) (setq command t)) + ((string-match "--env=\\(.+\\)" arg) + (push (match-string 1 arg) envs)) + ((string-match "-- =\\(.+\\)" arg) + (setq args (split-string-and-unquote (match-string 1 arg)))))) + (unless image + (setq image (read-string "Image to run: " nil 'kubed-container-images-history))) + (list pod image namespace port attach stdin tty rm envs command args))) + (if attach + (pop-to-buffer + (apply #'make-comint "kubed-run" kubed-kubectl-program nil + "run" pod (concat "--image=" image) "--attach" + (append + (mapcar (lambda (env) (concat "--env=" env)) + (cons "TERM=dumb" envs)) + (when namespace (list (concat "--namespace=" namespace))) + (when stdin '("-i")) + (when tty '("-t")) + (when rm '("--rm")) + (when port (list (format "--port=%d" port))) + (when command '("--command")) + (when args (cons "--" args))))) + (unless (zerop + (apply #'call-process + kubed-kubectl-program nil nil nil + "run" pod (concat "--image=" image) + (append + (mapcar (lambda (env) (concat "--env=" env)) envs) + (when namespace (list (concat "--namespace=" namespace))) + (when stdin '("-i")) + (cond + (attach '("--attach")) + (stdin '("--attach=false"))) + (when tty '("-t")) + (when rm '("--rm")) + (when port (list (format "--port=%d" port))) + (when command '("--command")) + (when args (cons "--" args))))) + (user-error "Failed to run image `%s'" image)) + (message "Image `%s' is now running in pod `%s'." image pod)) + (kubed-update-pods)) + +(defun kubed-pod-containers (pod &optional k8sns) + "Return list of containers in Kubernetes pod POD in namespace K8SNS." + (string-split + (car (process-lines + kubed-kubectl-program "get" + (if k8sns (concat "--namespace=" k8sns) "--all-namespaces=false") + "pod" pod "-o" "jsonpath={.spec.containers[*].name}")) + " ")) + +(defun kubed-pod-default-container (pod &optional k8sns) + "Return default container of Kubernetes pod POD in namespace K8SNS, or nil." + (car (process-lines + kubed-kubectl-program + "get" + (if k8sns (concat "--namespace=" k8sns) "--all-namespaces=false") + "pod" pod "-o" + "jsonpath={.metadata.annotations.kubectl\\.kubernetes\\.io/default-container}"))) + +(defun kubed-read-container (pod prompt &optional guess k8sns) + "Prompt with PROMPT for a container in POD and return its name. + +Non-nil optional argument GUESS says to try and guess which container to +use without prompting: if the pod has a +\"kubectl.kubernetes.id/default-container\" annotation, use the +container that this annotation specifes; if there's just one container, +use it; otherwise, fall back to prompting." + (let ((default (kubed-pod-default-container pod k8sns)) + (all 'unset)) + (or + ;; There's a default container, so that's our guess. + (and guess default) + ;; No default, but we're allowed to guess, so check if there's just + ;; one container, and if so that's our guess. + (and guess (setq all (kubed-pod-containers pod k8sns)) + (null (cdr all)) + (car all)) + ;; No guessing, prompt. + (completing-read (format-prompt prompt default) + (completion-table-dynamic + (lambda (_) + (if (eq all 'unset) + (setq all (kubed-pod-containers pod k8sns)) + all))) + nil t nil nil default)))) + +;;;###autoload +(defun kubed-logs (pod container &optional k8sns) + "Show logs for container CONTAINER in Kubernetes pod POD." + (interactive + (if kubed-all-namespaces-mode + (let* ((p-s (kubed-read-namespaced-pod "Show logs for pod")) + (p (car p-s)) + (s (cadr p-s))) + (list p (kubed-read-container p "Container" nil s) s)) + (let* ((p (kubed-read-pod "Show logs for pod")) + (c (kubed-read-container p "Container"))) + (list p c)))) + (let ((buf (generate-new-buffer (format "*kubed-logs %s[%s] in %s*" pod container + (or k8sns "current namespace"))))) + (with-current-buffer buf (run-hooks 'kubed-logs-setup-hook)) + (if k8sns + (message "Getting logs for container `%s' in pod `%s' in namespace `%s'..." container pod k8sns) + (message "Getting logs for container `%s' in pod `%s'..." container pod)) + (start-process "*kubed-logs*" buf + kubed-kubectl-program "logs" + (if k8sns (concat "--namespace=" k8sns) "--tail=-1") + "-f" "-c" container pod) + (display-buffer buf))) + +(defvar kubed-port-forward-process-alist nil + "Alist of current port-forwarding descriptors and corresponding processes.") + +(defun kubed-port-forward-process-alist (&optional _ignored) + "Update and return value of variable `kubed-port-forward-process-alist'." + (setq kubed-port-forward-process-alist + (seq-filter (lambda (pair) + (process-live-p (cdr pair))) + kubed-port-forward-process-alist))) + +;;;###autoload +(defun kubed-forward-port-to-pod (pod local-port remote-port &optional k8sns) + "Forward LOCAL-PORT to REMOTE-PORT of Kubernetes pod POD." + (interactive + (if kubed-all-namespaces-mode + (let* ((p-s (kubed-read-namespaced-pod "Forward port to pod")) + (p (car p-s)) + (s (cadr p-s))) + (list p (read-number "Local port: ") (read-number "Remote port: ") s)) + (let* ((p (kubed-read-pod "Forward port to pod")) + (l (read-number "Local port: ")) + (r (read-number "Remote port: "))) + (list p l r)))) + (if k8sns + (message "Forwarding local port %d to remote port %d of pod `%s'..." + local-port remote-port pod) + (message "Forwarding local port %d to remote port %d of pod `%s' in namespace `%s'..." + local-port remote-port pod k8sns)) + (push + (cons + (format "pod %s %d:%d%s" + pod local-port remote-port + (if k8sns (concat " in " k8sns) "")) + (start-process "*kubed-port-forward*" nil + kubed-kubectl-program "port-forward" + (if k8sns (concat "--namespace=" k8sns) "--address=localhost") + pod (format "%d:%d" local-port remote-port))) + kubed-port-forward-process-alist)) + +(defun kubed-stop-port-forward (descriptor) + "Stop Kubernetes port-forwarding with descriptor DESCRIPTOR. + +DESCRIPTOR is a string that says which port-forwarding process to stop, +it has the format \"pod POD LOCAL-PORT:REMOTE-PORT\", where POD is the +name of the pod that is the target of the port-forwarding, LOCAL-PORT is +the local port that is being forwarded, and REMOTE-PORT is the +correspoding remote port of POD. + +Interactively, prompt for DESCRIPTOR with completion. If there is only +one port-forwarding process, stop that process without prompting." + (interactive + (list + (cond + ((cdr (kubed-port-forward-process-alist)) + (completing-read "Stop port-forwarding: " + (completion-table-dynamic + #'kubed-port-forward-process-alist) + nil t)) + ((caar kubed-port-forward-process-alist)) + (t (user-error "No port-forwarding to Kubernetes in progress"))))) + (if-let ((pair (assoc descriptor kubed-port-forward-process-alist))) + (delete-process (cdr pair)) + (error "No port-forwarding for %s" descriptor)) + (message "Stopped port-forwarding for %s" descriptor)) + +(defvar kubed-container-images-history nil + "Minibuffer history for `kubed-read-container-images'.") + +(defun kubed-read-container-images (prompt &optional default) + "Prompt with PROMPT for names of container images. + +Optional argument DEFAULT is the minibuffer default argument." + (completing-read-multiple + (format-prompt prompt default) nil nil nil nil + 'kubed-container-images-history default)) + +(defvar kubed-ingress-rule-history nil + "Minibuffer history for `kubed-read-ingress-rules'.") + +(defun kubed-read-ingress-rules () + "Prompt with PROMPT for Kubernetes ingress rules." + (let ((rules (list (read-string "Ingress rule: " + nil 'kubed-ingress-rule-history)))) + (while (not + (string-empty-p + (car (push (read-string (format-prompt "Additional rules" "done") + nil 'kubed-ingress-rule-history) + rules))))) + (nreverse (cdr rules)))) + +(defvar transient-current-command) + +(defun kubed-transient-args (&optional prefix) + "Return current arguments from transient PREFIX. + +If PREFIX nil, it defaults to the value of `transient-current-command'." + (when-let ((prefix (or prefix (bound-and-true-p transient-current-command)))) + (and (featurep 'kubed-transient) + (fboundp 'transient-args) + (transient-args prefix)))) + +;;;###autoload +(defun kubed-attach (pod &optional container namespace stdin tty) + "Attach to running process in CONTAINER in Kubernetes POD. + +Optional argument NAMESPACE is the namespace in which to look for POD. +Non-nil STDIN says to connect local standard input to remote process. +Non-nil TTY says to use a TTY for standard input. + +Interactively, prompt for POD; if there are multiple pod containers, +prompt for CONTAINER as well; STDIN is t unless you call this command +with \\[universal-argument] \\[universal-argument]; and TTY is t unless\ + you call this command with \\[universal-argument]." + (interactive + (let ((namespace nil) (stdin t) (tty t)) + (when (<= 4 (prefix-numeric-value current-prefix-arg)) (setq tty nil)) + (when (<= 16 (prefix-numeric-value current-prefix-arg)) (setq stdin nil)) + (dolist (arg (kubed-transient-args 'kubed-transient-attach)) + (cond + ((string-match "--namespace=\\(.+\\)" arg) + (setq namespace (match-string 1 arg))) + ((equal "--stdin" arg) (setq stdin t)) + ((equal "--tty" arg) (setq tty t)))) + (if (and kubed-all-namespaces-mode (not namespace)) + (let* ((p-s (kubed-read-namespaced-pod "Attach to pod")) + (p (car p-s)) + (s (cadr p-s))) + (list p (kubed-read-container p "Container" t s) s stdin tty)) + ;; FIXME: When namespace is set from transient prefix, read pod + ;; name in that namespace instead. + (let* ((p (kubed-read-pod "Attach to pod")) + (c (kubed-read-container p "Container" t namespace))) + (list p c namespace stdin tty))))) + (pop-to-buffer + (apply #'make-comint "kubed-attach" kubed-kubectl-program nil + "attach" pod + (append + (when namespace (list "-n" namespace)) + (when container (list "-c" container)) + (when stdin '("-i")) + (when tty '("-t")))))) + +;;;###autoload +(defun kubed-diff (definition &optional include-managed) + "Display difference between Kubernetes resource DEFINITION and current state. + +Non-nil optional argument INCLUDE-MANAGED (interactively, the prefix +argument) says to include managed fields in the comparison." + (interactive + (let ((definition nil) (include-managed nil)) + (dolist (arg (when (and (fboundp 'transient-args) + (fboundp 'kubed-transient-diff)) + (transient-args 'kubed-transient-diff))) + (cond + ((string-match "--filename=\\(.+\\)" arg) + (setq definition (match-string 1 arg))) + ((equal "--show-managed-fields" arg) (setq include-managed t)))) + (list (or definition (kubed-read-resource-definition-file-name)) + (or include-managed current-prefix-arg)))) + (let ((buf (get-buffer-create "*kubed-diff*"))) + (with-current-buffer buf + (setq buffer-read-only nil) + (delete-region (point-min) (point-max)) + (fundamental-mode) + (call-process kubed-kubectl-program nil t nil "diff" + (concat "--show-managed-fields=" + (if include-managed "true" "false")) + "-f" (expand-file-name definition)) + (setq buffer-read-only t) + (diff-mode) + (goto-char (point-min))) + (display-buffer buf))) + +;;;###autoload +(defun kubed-exec (pod command &optional container namespace stdin tty args) + "Execute COMMAND with ARGS in CONTAINER in Kubernetes POD. + +Optional argument NAMESPACE is the namespace in which to look for POD. +Non-nil STDIN says to connect local standard input to remote process. +Non-nil TTY says to use a TTY for standard input. + +Interactively, prompt for POD; if there are multiple pod containers, +prompt for CONTAINER as well; STDIN is t unless you call this command +with \\[universal-argument] \\[universal-argument]; and TTY is t unless\ + you call this command with \\[universal-argument]." + (interactive + (let ((namespace nil) (stdin t) (tty t) (command nil) (args nil)) + (when (<= 4 (prefix-numeric-value current-prefix-arg)) (setq tty nil)) + (when (<= 16 (prefix-numeric-value current-prefix-arg)) (setq stdin nil)) + (dolist (arg (kubed-transient-args 'kubed-transient-exec)) + (cond + ((string-match "--namespace=\\(.+\\)" arg) + (setq namespace (match-string 1 arg))) + ((equal "--stdin" arg) (setq stdin t)) + ((equal "--tty" arg) (setq tty t)) + ((string-match "-- =\\(.+\\)" arg) + (setq args (split-string-and-unquote (match-string 1 arg)) + command (car args) + args (cdr args))))) + (if (and kubed-all-namespaces-mode (not namespace)) + (let* ((p-s (kubed-read-namespaced-pod "Execute command in pod")) + (p (car p-s)) + (s (cadr p-s)) + (c (kubed-read-container p "Container" t s))) + (unless command + (setq args (split-string-and-unquote + (read-string "Execute command: ")) + command (car args) + args (cdr args))) + (list p command c s stdin tty args)) + ;; FIXME: Similarly to `kubed-attach', when namespace is set from + ;; transient prefix, read pod name in that namespace instead. + (let* ((p (kubed-read-pod "Execute command in pod")) + (c (kubed-read-container p "Container" t namespace))) + (unless command + (setq args (split-string-and-unquote + (read-string "Execute command: ")) + command (car args) + args (cdr args))) + (list p command c namespace stdin tty args))))) + (pop-to-buffer + (apply #'make-comint "kubed-exec" kubed-kubectl-program nil + "exec" pod + (append + (when namespace (list "-n" namespace)) + (when container (list "-c" container)) + (when stdin '("-i")) + (when tty '("-t")) + (list "--" command) + args)))) + +(with-eval-after-load 'help-mode + ;; Wait for `help-mode' to define `help-xref'. It's always loaded by + ;; the time we actually need it in `kubed-explain'. + (define-button-type 'kubed-explain + :supertype 'help-xref + 'help-function 'kubed-explain + 'help-echo "mouse-2, RET: explain")) + +(defvar kubed-resource-field-history nil + "Minibuffer history for `kubed-read-resource-field'.") + +(defun kubed-read-resource-field (prompt &optional default) + "Prompt with PROMPT for Kubernetes resource type or field name. + +Optional argument DEFAULT is the minibuffer default argument." + (completing-read + (format-prompt prompt default) + (lambda (s p a) + (unless (eq a 'metadata) + (let ((start 0)) + (while (string-match "\\." s start) + (setq start (match-end 0))) + (if (eq (car-safe a) 'boundaries) + `(boundaries ,start . ,(and (string-match "\\." (cdr a)) + (match-beginning 0))) + (let ((table + (if (zerop start) + ;; Complete resource type. + (mapcar + (lambda (line) + (car (split-string line))) + (process-lines + kubed-kubectl-program + "api-resources" "--no-headers")) + ;; Complete (sub-)field name. + (with-temp-buffer + (call-process + kubed-kubectl-program nil t nil + "explain" (substring s 0 start)) + (goto-char (point-min)) + (let ((res nil)) + (while (re-search-forward + (rx line-start (+ " ") (group-n 1 (* alnum)) "\t") + nil t) + (push (match-string 1) res)) + res))))) + (if a (complete-with-action a table (substring s start) p) + ;; `try-completion'. + (let ((comp (complete-with-action a table (substring s start) p))) + (if (stringp comp) (concat (substring s 0 start) comp) comp)))))))) + nil 'confirm nil 'kubed-resource-field-history)) + +;;;###autoload +(defun kubed-explain (field) + "Show help buffer with explanation about Kubernetes resource FIELD." + (interactive + (list (kubed-read-resource-field "Explain type or field"))) + (let ((help-buffer-under-preparation t)) + (help-setup-xref (list #'kubed-explain field) + (called-interactively-p 'interactive)) + (with-help-window (help-buffer) + (with-current-buffer (help-buffer) + (insert (substitute-quotes + (concat "`kubed explain " field "' says:\n\n"))) + (save-excursion + ;; Add button that goes to parent. + (goto-char (point-min)) + (when (re-search-forward (rx " " + (group-n 1 (* graph)) + "." + (+ (not (any ?.))) + line-end) + nil t) + (help-xref-button 1 'kubed-explain (match-string 1)))) + + (call-process kubed-kubectl-program nil t nil "explain" field) + ;; Buttonize references to other fields. + (goto-char (point-min)) + (while (re-search-forward (rx line-start + (+ " ") + (group-n 1 (* alnum)) + "\t") + nil t) + (help-xref-button 1 'kubed-explain + (concat field "." (match-string 1)))))))) + +(defvar kubed-kubectl-command-history nil + "Minibuffer history for `kubed-kubectl-command'.") + +;;;###autoload +(defun kubed-kubectl-command (command) + "Execute `kubectl' COMMAND. + +This function calls `shell-command' (which see) to do the work. + +Interactively, prompt for COMMAND with completion for `kubectl' arguments." + (interactive + (list (cobra-read-command-line + "Command: " + (concat + kubed-kubectl-program " " + (let ((args (kubed-transient-args)) + (scope (and (fboundp 'transient-scope) (transient-scope)))) + (when (or args scope) + (concat (string-join (append scope args) " ") " ")))) + 'kubed-kubectl-command-history))) + (shell-command command)) + +;;;###autoload (autoload 'kubed-prefix-map "kubed" nil t 'keymap) +(defvar-keymap kubed-prefix-map + :doc "Prefix keymap for Kubed commands." + :prefix 'kubed-prefix-map + "p" 'kubed-pod-prefix-map + "N" 'kubed-namespace-prefix-map + "s" 'kubed-service-prefix-map + "S" 'kubed-secret-prefix-map + "j" 'kubed-job-prefix-map + "d" 'kubed-deployment-prefix-map + "i" 'kubed-ingress-prefix-map + "c" 'kubed-cronjob-prefix-map + "C" #'kubed-use-context + "U" #'kubed-update-all + "A" #'kubed-all-namespaces-mode + "+" #'kubed-create + "*" #'kubed-apply + "R" #'kubed-run + "=" #'kubed-diff + "E" #'kubed-explain + "!" #'kubed-kubectl-command) + +(defvar reporter-prompt-for-summary-p) + +(defun kubed-submit-bug-report () + "Report a Kubed to the maintainers via mail." + (interactive) + (require 'reporter) + (let ((reporter-prompt-for-summary-p t)) + (reporter-submit-bug-report + "Kubed Development <~eshel/kubed-devel@lists.sr.ht>" + (format "Kubed") + '(kubed-kubectl-program) + nil nil + (propertize " " + 'display + (propertize + "Insert your bug report below. +If possible, specify where you got Emacs, kubectl and Kubed, +and include a recipe for reproducing your issue. +[This line and the above text are not included in your report.]" + 'face 'italic))))) + +(provide 'kubed) +;;; kubed.el ends here diff --git a/kubed.png b/kubed.png new file mode 100644 index 0000000..e9117df Binary files /dev/null and b/kubed.png differ diff --git a/kubed.texi b/kubed.texi new file mode 100644 index 0000000..d6baa65 --- /dev/null +++ b/kubed.texi @@ -0,0 +1,164 @@ +\input texinfo @c -*- texinfo -*- +@c %**start of header +@setfilename kubed.info +@settitle Kubed: Kubernetes, Emacs, done! +@documentencoding UTF-8 +@documentlanguage en +@set MAINTAINERSITE @uref{https://eshelyaron.com,maintainer webpage} +@set MAINTAINER Eshel Yaron +@set MAINTAINEREMAIL @email{me@eshelyaron.com} +@set MAINTAINERCONTACT @uref{mailto:me@eshelyaron.com,contact the maintainer} +@c %**end of header + +@copying +This manual is for Kubed 0.1.0, a rich Emacs interface for Kubernetes. + +Copyright @copyright{} 2024 Eshel Yaron. + +@quotation +Permission is granted to copy, distribute and/or modify this document +under the terms of the GNU Free Documentation License, Version 1.3 or +any later version published by the Free Software Foundation; with no +Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. +@end quotation +@end copying + +@dircategory Emacs +@direntry +* Kubed: (kubed). Kubernetes, Emacs, done! +@end direntry + +@finalout +@titlepage +@title Kubed: Kubernetes, Emacs, done! +@author Eshel Yaron (@email{me@@eshelyaron.com}) +@end titlepage + +@contents + +@ifnottex +@node Top +@top Kubed: Kubernetes, Emacs, done! + +This manual is for Kubed 0.1.0, a rich Emacs interface for Kubernetes. + +@end ifnottex + +@menu +* Overview:: Introduction to Kubed +* Getting Started:: First steps with Kubed +* Usage:: Using Kubed to interact with Kubernetes +* Contributing:: Get involved in Kubed development +* Indices:: +@end menu + +@node Overview +@chapter Overview + +@cindex Kubed +@cindex kubed +Kubed is a rich Kubernetes interface within Emacs. It helps you work +with your Kubernetes clusters and deployments with the full power of +@command{kubectl}, and with the comfort and confidence of an intuitive +interactive interface. + +You can use Kubed to: + +@itemize +@item +Browse and manage Kubernetes workloads +@item +Connect to pods and edit files or execute commands +@item +Create new resources, edit and delete them +@item +Get help about various Kubernetes objects +@item +@dots{} +@end itemize + +These features and others are documented in the rest of this manual, +along with many options that Kubed provides for you to customize its +behavior. + +@node Getting Started +@chapter Getting Started + +@cindex installation +Use your favorite Emacs package manager to install Kubed from Git. You +can clone the Kubed Git repository from any of the following locations: + +@itemize +@item +@url{https://git.sr.ht/~eshel/kubed} +@url{https://github.com/eshelyaron/kubed.git} +@url{git://git.eshelyaron.com/kubed.git} +@end itemize + +@cindex requirements +To get started with Kubed, all you need is @command{kubectl} and Emacs. + +@vindex kubed-kubectl-program +Kubed tries to find @command{kubectl} in the directories listed in the +Emacs variable @code{exec-path}. When Emacs is started from a shell, it +initializes @code{exec-path} from the shell's @env{PATH} environment +variable which normally includes the location of @command{kubectl} in +common @command{kubectl} installations. If Emacs doesn't find the +@command{kubectl} executable via @code{exec-path}, you can tell Kubed +where to find it by customizing @code{kubed-kubectl-program}. + +@node Usage +@chapter Usage + +This chapter is under construction. + +@node Contributing +@chapter Contributing + +We highly appreciate all contributions, including bug reports, +patches, improvement suggestions, and general feedback! + +The best way to get in touch with the Kubed maintainers is via +@uref{https://lists.sr.ht/~eshel/kubed-devel, the Kubed mailing list}. + +@findex kubed-submit-bug-report +@deffn Command kubed-submit-bug-report +Report a bug in Kubed to the maintainers via mail. +@end deffn + +You can use the command @kbd{M-x kubed-submit-bug-report} to easily +contact the Kubed maintainers from within Emacs. This command opens a +new buffer with a message template ready to be sent to the development +mailing list. + +@node Indices +@unnumbered Indices + +@menu +* Function Index:: +* Variable Index:: +* Keystroke Index:: +* Concept Index:: +@end menu + +@node Function Index +@unnumberedsec Function index + +@printindex fn + +@node Variable Index +@unnumberedsec Variable index + +@printindex vr + +@node Keystroke Index +@unnumberedsec Keystroke index + +@printindex ky + +@node Concept Index +@unnumberedsec Concept index + +@printindex cp + +@bye