]> git.eshelyaron.com Git - kubed.git/commitdiff
Initial commit
authorEshel Yaron <me@eshelyaron.com>
Sun, 28 Jul 2024 10:16:03 +0000 (12:16 +0200)
committerEshel Yaron <me@eshelyaron.com>
Sun, 28 Jul 2024 10:16:03 +0000 (12:16 +0200)
.dir-locals.el [new file with mode: 0644]
.gitignore [new file with mode: 0644]
LICENSE [new file with mode: 0644]
NEWS.org [new file with mode: 0644]
README.md [new file with mode: 0644]
cobra.el [new file with mode: 0644]
kubed-transient.el [new file with mode: 0644]
kubed.el [new file with mode: 0644]
kubed.png [new file with mode: 0644]
kubed.texi [new file with mode: 0644]

diff --git a/.dir-locals.el b/.dir-locals.el
new file mode 100644 (file)
index 0000000..14646b3
--- /dev/null
@@ -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 (file)
index 0000000..b25c15b
--- /dev/null
@@ -0,0 +1 @@
+*~
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
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 (file)
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 (file)
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 (file)
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 <me@eshelyaron.com>
+;; 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 (file)
index 0000000..7154d18
--- /dev/null
@@ -0,0 +1,226 @@
+;;; kubed-transient.el --- Kubernetes transient menus   -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2024  Eshel Yaron
+
+;; Author: Eshel Yaron <me@eshelyaron.com>
+;; 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 (file)
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 <me@eshelyaron.com>
+;; 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 "<none>") "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 "<none>") "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 (file)
index 0000000..e9117df
Binary files /dev/null and b/kubed.png differ
diff --git a/kubed.texi b/kubed.texi
new file mode 100644 (file)
index 0000000..d6baa65
--- /dev/null
@@ -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