kx-cli 0.0.5__tar.gz → 0.0.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kx_cli-0.0.5/src/kx_cli.egg-info → kx_cli-0.0.7}/PKG-INFO +76 -11
- {kx_cli-0.0.5 → kx_cli-0.0.7}/README.md +71 -9
- {kx_cli-0.0.5 → kx_cli-0.0.7}/pyproject.toml +6 -2
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/delete.py +9 -2
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/describe.py +6 -2
- kx_cli-0.0.7/src/kx/commands/diagnostic.py +195 -0
- kx_cli-0.0.7/src/kx/commands/edit.py +18 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/events.py +12 -1
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/exec.py +5 -1
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/get.py +15 -5
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/logs.py +19 -4
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/port_forward.py +8 -2
- kx_cli-0.0.7/src/kx/commands/theme.py +23 -0
- kx_cli-0.0.7/src/kx/config.py +82 -0
- kx_cli-0.0.7/src/kx/console.py +708 -0
- kx_cli-0.0.7/src/kx/diagnostics.py +363 -0
- kx_cli-0.0.7/src/kx/errors.py +80 -0
- kx_cli-0.0.7/src/kx/graph.py +186 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/index.py +8 -14
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/kinds.py +4 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/main.py +276 -110
- kx_cli-0.0.7/src/kx/refresh.py +63 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/state.py +23 -2
- kx_cli-0.0.7/src/kx/themes.py +149 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/types.py +2 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7/src/kx_cli.egg-info}/PKG-INFO +76 -11
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx_cli.egg-info/SOURCES.txt +18 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx_cli.egg-info/requires.txt +5 -1
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_cli_get.py +14 -0
- kx_cli-0.0.7/tests/test_cli_help.py +60 -0
- kx_cli-0.0.7/tests/test_cli_kind_alias.py +155 -0
- kx_cli-0.0.7/tests/test_config.py +183 -0
- kx_cli-0.0.7/tests/test_console.py +645 -0
- kx_cli-0.0.7/tests/test_delete.py +61 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_describe.py +26 -0
- kx_cli-0.0.7/tests/test_diagnostic.py +241 -0
- kx_cli-0.0.7/tests/test_diagnostics.py +415 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_edit.py +26 -0
- kx_cli-0.0.7/tests/test_error_handling.py +193 -0
- kx_cli-0.0.7/tests/test_events.py +112 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_exec.py +36 -2
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_get.py +25 -0
- kx_cli-0.0.7/tests/test_graph.py +233 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_index.py +3 -4
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_logs.py +49 -0
- kx_cli-0.0.7/tests/test_namespace.py +105 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_port_forward.py +26 -0
- kx_cli-0.0.7/tests/test_refresh.py +82 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_state.py +52 -4
- kx_cli-0.0.7/tests/test_theme.py +40 -0
- kx_cli-0.0.7/tests/test_themes.py +36 -0
- kx_cli-0.0.7/tests/test_tree.py +59 -0
- kx_cli-0.0.5/src/kx/commands/edit.py +0 -12
- kx_cli-0.0.5/src/kx/config.py +0 -38
- kx_cli-0.0.5/src/kx/console.py +0 -368
- kx_cli-0.0.5/src/kx/graph.py +0 -231
- kx_cli-0.0.5/tests/test_config.py +0 -87
- kx_cli-0.0.5/tests/test_console.py +0 -202
- kx_cli-0.0.5/tests/test_namespace.py +0 -51
- {kx_cli-0.0.5 → kx_cli-0.0.7}/LICENSE +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/setup.cfg +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/__init__.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/__init__.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/back.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/drop.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/forward.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/labels.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/namespace.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/rollout.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/scale.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/state.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/tree.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/commands/yaml.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/events.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/k8s.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx/kubectl.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx_cli.egg-info/dependency_links.txt +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx_cli.egg-info/entry_points.txt +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/src/kx_cli.egg-info/top_level.txt +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_back.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_drop.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_forward.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_kinds.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_labels.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_rollout.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_scale.py +0 -0
- {kx_cli-0.0.5 → kx_cli-0.0.7}/tests/test_yaml.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kx-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.7
|
|
4
4
|
Summary: kubectl wrapper with index-based resource selection
|
|
5
5
|
Classifier: Programming Language :: Python :: 3
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -9,13 +9,16 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
9
9
|
Requires-Python: >=3.11
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
License-File: LICENSE
|
|
12
|
-
Requires-Dist: typer==0.
|
|
12
|
+
Requires-Dist: typer==0.27.0
|
|
13
13
|
Requires-Dist: kubernetes
|
|
14
14
|
Requires-Dist: rich
|
|
15
15
|
Provides-Extra: dev
|
|
16
16
|
Requires-Dist: ruff; extra == "dev"
|
|
17
17
|
Requires-Dist: pre-commit; extra == "dev"
|
|
18
18
|
Requires-Dist: pytest; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
20
|
+
Provides-Extra: build
|
|
21
|
+
Requires-Dist: pyinstaller; extra == "build"
|
|
19
22
|
Dynamic: license-file
|
|
20
23
|
|
|
21
24
|
<div align="center">
|
|
@@ -47,10 +50,42 @@ Dynamic: license-file
|
|
|
47
50
|
|
|
48
51
|
## Install
|
|
49
52
|
|
|
53
|
+
Requires Python 3.11+ and `kubectl` on your PATH.
|
|
54
|
+
|
|
55
|
+
With [uv](https://docs.astral.sh/uv/) (recommended):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv tool install kx-cli
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
With [pipx](https://pipx.pypa.io/):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pipx install kx-cli
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
With pip:
|
|
68
|
+
|
|
50
69
|
```bash
|
|
51
70
|
pip install kx-cli
|
|
52
71
|
```
|
|
53
72
|
|
|
73
|
+
As a kubectl plugin via [krew](https://krew.sigs.k8s.io/) (no Python required; pending krew-index acceptance):
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
kubectl krew install kx
|
|
77
|
+
alias kx="kubectl kx"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Standalone binaries (linux/macOS, amd64/arm64, no Python required) are attached to each [GitHub Release](https://github.com/jzills/kx/releases).
|
|
81
|
+
|
|
82
|
+
Or try it without installing (the package is `kx-cli`, the command is `kx`):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
uvx --from kx-cli kx get pods
|
|
86
|
+
pipx run --spec kx-cli kx get pods
|
|
87
|
+
```
|
|
88
|
+
|
|
54
89
|
## Usage
|
|
55
90
|
|
|
56
91
|
### List resources
|
|
@@ -59,29 +94,34 @@ pip install kx-cli
|
|
|
59
94
|
kx get <resource> [--match|-m <substring>] [kubectl flags...]
|
|
60
95
|
```
|
|
61
96
|
|
|
62
|
-
Fetches resources and assigns index numbers. Any extra flags (e.g. `-n <namespace>`, `-A`) are passed through to kubectl. Use `--match`/`-m` to filter results by name (substring, case-insensitive).
|
|
97
|
+
Fetches resources and assigns index numbers. Any extra flags (e.g. `-n <namespace>`, `-A`) are passed through to kubectl. Use `--match`/`-m` to filter results by name (substring, case-insensitive). All-namespace listings (`-A`) are display-only — rows aren't indexed, since names aren't unique across namespaces; scope to a namespace to select.
|
|
98
|
+
|
|
99
|
+
Known kinds can drop the `get`: `kx pods`, `kx deploy -n kube-system`, `kx svc --match api`. This covers the built-in kinds and their kubectl shorthands (`po`, `deploy`, `svc`, `sts`, ...); existing commands take precedence, so `kx ns 3` still switches namespaces (bare `kx ns` lists them). CRDs and other resource types still use `kx get <resource>`. An integer after a kind is an index into the current state: `kx po 3` (or `kx get po 3`) relists just that pod, erroring if index 3 isn't a pod. Multiple indexes work too: `kx po 1 3`.
|
|
63
100
|
|
|
64
101
|
```
|
|
65
102
|
$ kx get pods
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
103
|
+
Pods · default · 3 items
|
|
104
|
+
X NAME READY STATUS RESTARTS AGE
|
|
105
|
+
1 api-7d9f4b8c6-xkp2q 1/1 Running 0 2d
|
|
106
|
+
2 worker-6c8b5f7d9-mnt4r 1/1 Running 3 5h
|
|
107
|
+
3 postgres-0 1/1 Running 0 12d
|
|
70
108
|
```
|
|
71
109
|
|
|
72
110
|
All subsequent commands reference resources by their `X` index from the last `kx get`.
|
|
73
111
|
|
|
112
|
+
Global flags: `--no-color` disables styled output, `-v`/`--version` prints the installed version, and `--help` on any command shows usage, examples, and aliases.
|
|
113
|
+
|
|
74
114
|
### Commands
|
|
75
115
|
|
|
76
116
|
<!-- commands-table-start -->
|
|
77
117
|
| Command | Description |
|
|
78
118
|
|---|---|
|
|
79
|
-
| `kx get <resource> [--match/-m
|
|
119
|
+
| `kx get <resource> [--match/-m str] [kubectl flags...]` | List resources and assign index numbers for use with other commands; shorthand: kx <kind> (e.g. kx pods, kx po 3). |
|
|
80
120
|
| `kx describe <indexes>... [kubectl flags...]` | Show full kubectl describe output for one or more indexed resources. |
|
|
81
121
|
| `kx events <indexes>...` | Show Kubernetes events for one or more indexed resources. |
|
|
82
122
|
| `kx logs <index> [kubectl flags...]` | Stream logs for an indexed resource; aggregates across pods for Deployments, StatefulSets, DaemonSets, and Services. |
|
|
83
123
|
| `kx labels <indexes>... [--selector/-s]` | Show labels for one or more indexed resources; --selector formats output as a label selector. |
|
|
84
|
-
| `kx yaml <indexes>... [--show
|
|
124
|
+
| `kx yaml <indexes>... [--show str]` | Print the raw YAML manifest for one or more indexed resources; --show filters to specific top-level fields. |
|
|
85
125
|
| `kx delete <indexes>... [--yes/-y]` | Delete one or more indexed resources (prompts for confirmation unless --yes). |
|
|
86
126
|
| `kx edit <index> [kubectl flags...]` | Open an indexed resource in your editor via kubectl edit. |
|
|
87
127
|
| `kx exec <index> [<cmd>...] [kubectl flags...]` | Open an interactive shell in an indexed pod (bash, falling back to sh). |
|
|
@@ -89,7 +129,9 @@ All subsequent commands reference resources by their `X` index from the last `kx
|
|
|
89
129
|
| `kx rollout <action> <index>` | Run a rollout action (status, restart, pause, resume, history, undo) on a Deployment, StatefulSet, or DaemonSet. |
|
|
90
130
|
| `kx scale <index> <replicas>` | Scale an indexed Deployment, StatefulSet, or ReplicaSet to a given replica count. |
|
|
91
131
|
| `kx port-forward <index> <port> [kubectl flags...]` | Forward a local port to an indexed resource (Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Service). |
|
|
92
|
-
| `kx
|
|
132
|
+
| `kx diagnostic <index>` | Run read-only health diagnostics on an indexed Deployment, StatefulSet, DaemonSet, or Pod; alias: kx diag. |
|
|
133
|
+
| `kx namespace [<index>]` | List namespaces, or switch to an indexed one; alias: kx ns. |
|
|
134
|
+
| `kx theme [<name>]` | List available color themes or persist a choice by name or index. |
|
|
93
135
|
| `kx state [<position>] [--all/-a]` | Show current state, jump to a history position, or list all entries with --all. |
|
|
94
136
|
| `kx drop <position>` | Remove a history entry by position (shown in kx state --all). |
|
|
95
137
|
| `kx back` | Navigate to the previous kx get result. |
|
|
@@ -128,7 +170,7 @@ kx delete 3
|
|
|
128
170
|
|
|
129
171
|
## State
|
|
130
172
|
|
|
131
|
-
`kx` maintains a history of up to 10 `kx get` results in `~/.
|
|
173
|
+
`kx` maintains a history of up to 10 `kx get` results in `~/.kx/state.json`. A cursor tracks your current position; index-based commands always resolve against the entry at the cursor.
|
|
132
174
|
|
|
133
175
|
```
|
|
134
176
|
$ kx get pods # saves a new entry, cursor advances
|
|
@@ -139,6 +181,29 @@ $ kx state --all # lists all history entries and the current position
|
|
|
139
181
|
|
|
140
182
|
Use `kx state <position>` to jump directly to any history entry, and `kx drop <position>` to remove one.
|
|
141
183
|
|
|
184
|
+
## Configuration
|
|
185
|
+
|
|
186
|
+
`kx` reads `~/.kx/config.toml`; environment variables override file settings.
|
|
187
|
+
|
|
188
|
+
| Key | Env var | Default | Description |
|
|
189
|
+
| --- | --- | --- | --- |
|
|
190
|
+
| `max_history` | `KX_MAX_HISTORY` | `10` | Number of `kx get` results kept in history. |
|
|
191
|
+
| `shells` | `KX_SHELLS` (comma-separated) | `["bash", "sh"]` | Shell candidates for `kx exec`. |
|
|
192
|
+
| `no_color` | `KX_NO_COLOR` | `false` | Disable styled output (same as `--no-color`). |
|
|
193
|
+
| `theme` | `KX_THEME` | `"github-dark"` | Color theme for all output. |
|
|
194
|
+
|
|
195
|
+
Styled output is emitted only when stdout is a terminal — piped or redirected output is plain text, so `kx get pods | grep worker` stays clean. The [`NO_COLOR`](https://no-color.org/) convention is honored as well.
|
|
196
|
+
|
|
197
|
+
### Themes
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
kx theme # list available themes (indexed) with a preview of each
|
|
201
|
+
kx theme nord # persist a theme to ~/.kx/config.toml
|
|
202
|
+
kx theme 3 # same, selecting by index from the kx theme listing
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Prefab themes: `github-dark` (default), `dracula`, `nord`, `gruvbox`, `solarized-dark`, `catppuccin-mocha`, `tokyo-night`, `rose-pine`, `mono`, `light` (for light terminal backgrounds), and `plain` (no styling at all).
|
|
206
|
+
|
|
142
207
|
## Development
|
|
143
208
|
|
|
144
209
|
```bash
|
|
@@ -27,10 +27,42 @@
|
|
|
27
27
|
|
|
28
28
|
## Install
|
|
29
29
|
|
|
30
|
+
Requires Python 3.11+ and `kubectl` on your PATH.
|
|
31
|
+
|
|
32
|
+
With [uv](https://docs.astral.sh/uv/) (recommended):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv tool install kx-cli
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
With [pipx](https://pipx.pypa.io/):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pipx install kx-cli
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
With pip:
|
|
45
|
+
|
|
30
46
|
```bash
|
|
31
47
|
pip install kx-cli
|
|
32
48
|
```
|
|
33
49
|
|
|
50
|
+
As a kubectl plugin via [krew](https://krew.sigs.k8s.io/) (no Python required; pending krew-index acceptance):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
kubectl krew install kx
|
|
54
|
+
alias kx="kubectl kx"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Standalone binaries (linux/macOS, amd64/arm64, no Python required) are attached to each [GitHub Release](https://github.com/jzills/kx/releases).
|
|
58
|
+
|
|
59
|
+
Or try it without installing (the package is `kx-cli`, the command is `kx`):
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uvx --from kx-cli kx get pods
|
|
63
|
+
pipx run --spec kx-cli kx get pods
|
|
64
|
+
```
|
|
65
|
+
|
|
34
66
|
## Usage
|
|
35
67
|
|
|
36
68
|
### List resources
|
|
@@ -39,29 +71,34 @@ pip install kx-cli
|
|
|
39
71
|
kx get <resource> [--match|-m <substring>] [kubectl flags...]
|
|
40
72
|
```
|
|
41
73
|
|
|
42
|
-
Fetches resources and assigns index numbers. Any extra flags (e.g. `-n <namespace>`, `-A`) are passed through to kubectl. Use `--match`/`-m` to filter results by name (substring, case-insensitive).
|
|
74
|
+
Fetches resources and assigns index numbers. Any extra flags (e.g. `-n <namespace>`, `-A`) are passed through to kubectl. Use `--match`/`-m` to filter results by name (substring, case-insensitive). All-namespace listings (`-A`) are display-only — rows aren't indexed, since names aren't unique across namespaces; scope to a namespace to select.
|
|
75
|
+
|
|
76
|
+
Known kinds can drop the `get`: `kx pods`, `kx deploy -n kube-system`, `kx svc --match api`. This covers the built-in kinds and their kubectl shorthands (`po`, `deploy`, `svc`, `sts`, ...); existing commands take precedence, so `kx ns 3` still switches namespaces (bare `kx ns` lists them). CRDs and other resource types still use `kx get <resource>`. An integer after a kind is an index into the current state: `kx po 3` (or `kx get po 3`) relists just that pod, erroring if index 3 isn't a pod. Multiple indexes work too: `kx po 1 3`.
|
|
43
77
|
|
|
44
78
|
```
|
|
45
79
|
$ kx get pods
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
80
|
+
Pods · default · 3 items
|
|
81
|
+
X NAME READY STATUS RESTARTS AGE
|
|
82
|
+
1 api-7d9f4b8c6-xkp2q 1/1 Running 0 2d
|
|
83
|
+
2 worker-6c8b5f7d9-mnt4r 1/1 Running 3 5h
|
|
84
|
+
3 postgres-0 1/1 Running 0 12d
|
|
50
85
|
```
|
|
51
86
|
|
|
52
87
|
All subsequent commands reference resources by their `X` index from the last `kx get`.
|
|
53
88
|
|
|
89
|
+
Global flags: `--no-color` disables styled output, `-v`/`--version` prints the installed version, and `--help` on any command shows usage, examples, and aliases.
|
|
90
|
+
|
|
54
91
|
### Commands
|
|
55
92
|
|
|
56
93
|
<!-- commands-table-start -->
|
|
57
94
|
| Command | Description |
|
|
58
95
|
|---|---|
|
|
59
|
-
| `kx get <resource> [--match/-m
|
|
96
|
+
| `kx get <resource> [--match/-m str] [kubectl flags...]` | List resources and assign index numbers for use with other commands; shorthand: kx <kind> (e.g. kx pods, kx po 3). |
|
|
60
97
|
| `kx describe <indexes>... [kubectl flags...]` | Show full kubectl describe output for one or more indexed resources. |
|
|
61
98
|
| `kx events <indexes>...` | Show Kubernetes events for one or more indexed resources. |
|
|
62
99
|
| `kx logs <index> [kubectl flags...]` | Stream logs for an indexed resource; aggregates across pods for Deployments, StatefulSets, DaemonSets, and Services. |
|
|
63
100
|
| `kx labels <indexes>... [--selector/-s]` | Show labels for one or more indexed resources; --selector formats output as a label selector. |
|
|
64
|
-
| `kx yaml <indexes>... [--show
|
|
101
|
+
| `kx yaml <indexes>... [--show str]` | Print the raw YAML manifest for one or more indexed resources; --show filters to specific top-level fields. |
|
|
65
102
|
| `kx delete <indexes>... [--yes/-y]` | Delete one or more indexed resources (prompts for confirmation unless --yes). |
|
|
66
103
|
| `kx edit <index> [kubectl flags...]` | Open an indexed resource in your editor via kubectl edit. |
|
|
67
104
|
| `kx exec <index> [<cmd>...] [kubectl flags...]` | Open an interactive shell in an indexed pod (bash, falling back to sh). |
|
|
@@ -69,7 +106,9 @@ All subsequent commands reference resources by their `X` index from the last `kx
|
|
|
69
106
|
| `kx rollout <action> <index>` | Run a rollout action (status, restart, pause, resume, history, undo) on a Deployment, StatefulSet, or DaemonSet. |
|
|
70
107
|
| `kx scale <index> <replicas>` | Scale an indexed Deployment, StatefulSet, or ReplicaSet to a given replica count. |
|
|
71
108
|
| `kx port-forward <index> <port> [kubectl flags...]` | Forward a local port to an indexed resource (Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Service). |
|
|
72
|
-
| `kx
|
|
109
|
+
| `kx diagnostic <index>` | Run read-only health diagnostics on an indexed Deployment, StatefulSet, DaemonSet, or Pod; alias: kx diag. |
|
|
110
|
+
| `kx namespace [<index>]` | List namespaces, or switch to an indexed one; alias: kx ns. |
|
|
111
|
+
| `kx theme [<name>]` | List available color themes or persist a choice by name or index. |
|
|
73
112
|
| `kx state [<position>] [--all/-a]` | Show current state, jump to a history position, or list all entries with --all. |
|
|
74
113
|
| `kx drop <position>` | Remove a history entry by position (shown in kx state --all). |
|
|
75
114
|
| `kx back` | Navigate to the previous kx get result. |
|
|
@@ -108,7 +147,7 @@ kx delete 3
|
|
|
108
147
|
|
|
109
148
|
## State
|
|
110
149
|
|
|
111
|
-
`kx` maintains a history of up to 10 `kx get` results in `~/.
|
|
150
|
+
`kx` maintains a history of up to 10 `kx get` results in `~/.kx/state.json`. A cursor tracks your current position; index-based commands always resolve against the entry at the cursor.
|
|
112
151
|
|
|
113
152
|
```
|
|
114
153
|
$ kx get pods # saves a new entry, cursor advances
|
|
@@ -119,6 +158,29 @@ $ kx state --all # lists all history entries and the current position
|
|
|
119
158
|
|
|
120
159
|
Use `kx state <position>` to jump directly to any history entry, and `kx drop <position>` to remove one.
|
|
121
160
|
|
|
161
|
+
## Configuration
|
|
162
|
+
|
|
163
|
+
`kx` reads `~/.kx/config.toml`; environment variables override file settings.
|
|
164
|
+
|
|
165
|
+
| Key | Env var | Default | Description |
|
|
166
|
+
| --- | --- | --- | --- |
|
|
167
|
+
| `max_history` | `KX_MAX_HISTORY` | `10` | Number of `kx get` results kept in history. |
|
|
168
|
+
| `shells` | `KX_SHELLS` (comma-separated) | `["bash", "sh"]` | Shell candidates for `kx exec`. |
|
|
169
|
+
| `no_color` | `KX_NO_COLOR` | `false` | Disable styled output (same as `--no-color`). |
|
|
170
|
+
| `theme` | `KX_THEME` | `"github-dark"` | Color theme for all output. |
|
|
171
|
+
|
|
172
|
+
Styled output is emitted only when stdout is a terminal — piped or redirected output is plain text, so `kx get pods | grep worker` stays clean. The [`NO_COLOR`](https://no-color.org/) convention is honored as well.
|
|
173
|
+
|
|
174
|
+
### Themes
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
kx theme # list available themes (indexed) with a preview of each
|
|
178
|
+
kx theme nord # persist a theme to ~/.kx/config.toml
|
|
179
|
+
kx theme 3 # same, selecting by index from the kx theme listing
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Prefab themes: `github-dark` (default), `dracula`, `nord`, `gruvbox`, `solarized-dark`, `catppuccin-mocha`, `tokyo-night`, `rose-pine`, `mono`, `light` (for light terminal backgrounds), and `plain` (no styling at all).
|
|
183
|
+
|
|
122
184
|
## Development
|
|
123
185
|
|
|
124
186
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "kx-cli"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.7"
|
|
4
4
|
description = "kubectl wrapper with index-based resource selection"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -11,7 +11,7 @@ classifiers = [
|
|
|
11
11
|
"Programming Language :: Python :: 3.13",
|
|
12
12
|
]
|
|
13
13
|
dependencies = [
|
|
14
|
-
"typer==0.
|
|
14
|
+
"typer==0.27.0",
|
|
15
15
|
"kubernetes",
|
|
16
16
|
"rich",
|
|
17
17
|
]
|
|
@@ -21,6 +21,10 @@ dev = [
|
|
|
21
21
|
"ruff",
|
|
22
22
|
"pre-commit",
|
|
23
23
|
"pytest",
|
|
24
|
+
"pytest-cov",
|
|
25
|
+
]
|
|
26
|
+
build = [
|
|
27
|
+
"pyinstaller",
|
|
24
28
|
]
|
|
25
29
|
|
|
26
30
|
[project.scripts]
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from contextlib import nullcontext
|
|
2
|
+
|
|
1
3
|
from kx.kubectl import KubectlServiceProtocol
|
|
2
4
|
from kx.state import StateServiceProtocol
|
|
3
|
-
from kx.types import Confirm
|
|
5
|
+
from kx.types import Confirm, Status
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class DeleteCommand:
|
|
@@ -9,14 +11,19 @@ class DeleteCommand:
|
|
|
9
11
|
state: StateServiceProtocol,
|
|
10
12
|
kubectl: KubectlServiceProtocol,
|
|
11
13
|
confirm: Confirm,
|
|
14
|
+
status: Status | None = None,
|
|
12
15
|
):
|
|
13
16
|
self.state = state
|
|
14
17
|
self.kubectl = kubectl
|
|
15
18
|
self.confirm = confirm
|
|
19
|
+
self.status = status or (lambda _msg: nullcontext())
|
|
16
20
|
|
|
17
21
|
def execute(self, index: int, yes: bool) -> str:
|
|
18
22
|
name, namespace, kind = self.state.fields(index)
|
|
23
|
+
# The confirm prompt must stay outside the spinner: a prompt inside a
|
|
24
|
+
# Live region breaks input.
|
|
19
25
|
if not yes:
|
|
20
26
|
self.confirm(f"Delete {kind}/{name} in {namespace}?")
|
|
21
|
-
self.
|
|
27
|
+
with self.status("deleting"):
|
|
28
|
+
self.kubectl.run(["delete", kind, name, "-n", namespace])
|
|
22
29
|
return f"Deleted {kind}/{name}"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from kx.kubectl import KubectlServiceProtocol
|
|
2
|
+
from kx.refresh import ensure_exists
|
|
2
3
|
from kx.state import StateServiceProtocol
|
|
3
4
|
|
|
4
5
|
|
|
@@ -11,8 +12,11 @@ class DescribeCommand:
|
|
|
11
12
|
self.state = state
|
|
12
13
|
self.kubectl = kubectl
|
|
13
14
|
|
|
14
|
-
def execute(self, index: int, extra_args: list[str] =
|
|
15
|
+
def execute(self, index: int, extra_args: list[str] | None = None) -> None:
|
|
16
|
+
extra_args = extra_args or []
|
|
15
17
|
name, namespace, kind = self.state.fields(index)
|
|
16
|
-
self.kubectl.run_interactive(
|
|
18
|
+
rc = self.kubectl.run_interactive(
|
|
17
19
|
["describe", kind, name, "-n", namespace, *extra_args]
|
|
18
20
|
)
|
|
21
|
+
if rc != 0:
|
|
22
|
+
ensure_exists(self.kubectl, kind, name, namespace)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from kx.diagnostics import (
|
|
2
|
+
ContainerDiagnostic,
|
|
3
|
+
DiagnosticData,
|
|
4
|
+
DiagnosticReport,
|
|
5
|
+
DiagnosticsServiceProtocol,
|
|
6
|
+
Finding,
|
|
7
|
+
PodDiagnostic,
|
|
8
|
+
ReplicaHealth,
|
|
9
|
+
Severity,
|
|
10
|
+
)
|
|
11
|
+
from kx.kinds import Kind
|
|
12
|
+
from kx.state import StateServiceProtocol
|
|
13
|
+
|
|
14
|
+
_SUPPORTED_KINDS = {Kind.Deployment, Kind.StatefulSet, Kind.DaemonSet, Kind.Pod}
|
|
15
|
+
_RESTART_WARN_THRESHOLD = 5
|
|
16
|
+
|
|
17
|
+
_IMAGE_PULL_REASONS = {"ImagePullBackOff", "ErrImagePull", "InvalidImageName"}
|
|
18
|
+
_CONFIG_ERROR_REASONS = {"CreateContainerConfigError", "CreateContainerError"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DiagnosticCommand:
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
state: StateServiceProtocol,
|
|
25
|
+
diagnostics: DiagnosticsServiceProtocol,
|
|
26
|
+
):
|
|
27
|
+
self.state = state
|
|
28
|
+
self.diagnostics = diagnostics
|
|
29
|
+
|
|
30
|
+
def execute(self, index: int) -> DiagnosticReport:
|
|
31
|
+
name, namespace, kind = self.state.fields(index)
|
|
32
|
+
if kind not in _SUPPORTED_KINDS:
|
|
33
|
+
raise ValueError(f"diagnostic is not supported for '{kind}'.")
|
|
34
|
+
return build_report(self.diagnostics.gather(kind, name, namespace))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_report(data: DiagnosticData) -> DiagnosticReport:
|
|
38
|
+
findings: list[Finding] = []
|
|
39
|
+
|
|
40
|
+
if data.replicas is not None:
|
|
41
|
+
findings.extend(_replica_findings(data.replicas))
|
|
42
|
+
for pod in data.pods:
|
|
43
|
+
findings.extend(_pod_findings(pod))
|
|
44
|
+
findings.extend(_event_findings(data.warning_events))
|
|
45
|
+
|
|
46
|
+
# Prioritize: highest severity first, stable within a severity.
|
|
47
|
+
findings.sort(key=lambda f: f.severity, reverse=True)
|
|
48
|
+
verdict = max((f.severity for f in findings), default=Severity.OK)
|
|
49
|
+
|
|
50
|
+
return DiagnosticReport(
|
|
51
|
+
kind=data.kind,
|
|
52
|
+
name=data.name,
|
|
53
|
+
namespace=data.namespace,
|
|
54
|
+
verdict=verdict,
|
|
55
|
+
findings=findings,
|
|
56
|
+
pods=data.pods,
|
|
57
|
+
warning_events=data.warning_events,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _replica_findings(replicas: ReplicaHealth) -> list[Finding]:
|
|
62
|
+
findings: list[Finding] = []
|
|
63
|
+
gen = replicas.generation
|
|
64
|
+
observed = replicas.observed_generation
|
|
65
|
+
if gen is not None and observed is not None and observed < gen:
|
|
66
|
+
findings.append(
|
|
67
|
+
Finding(
|
|
68
|
+
Severity.CRITICAL,
|
|
69
|
+
f"Rollout stalled: observed generation {observed} behind spec generation {gen}",
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
if replicas.ready < replicas.desired:
|
|
73
|
+
severity = (
|
|
74
|
+
Severity.CRITICAL
|
|
75
|
+
if replicas.ready == 0 and replicas.desired > 0
|
|
76
|
+
else Severity.WARNING
|
|
77
|
+
)
|
|
78
|
+
findings.append(
|
|
79
|
+
Finding(
|
|
80
|
+
severity,
|
|
81
|
+
f"Only {replicas.ready}/{replicas.desired} replicas ready",
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
if replicas.available < replicas.desired:
|
|
85
|
+
findings.append(
|
|
86
|
+
Finding(
|
|
87
|
+
Severity.WARNING,
|
|
88
|
+
f"{replicas.available}/{replicas.desired} replicas available",
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
if replicas.updated < replicas.desired:
|
|
92
|
+
findings.append(
|
|
93
|
+
Finding(
|
|
94
|
+
Severity.WARNING,
|
|
95
|
+
f"Rollout in progress: {replicas.updated}/{replicas.desired} replicas updated",
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
return findings
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _pod_findings(pod: PodDiagnostic) -> list[Finding]:
|
|
102
|
+
findings: list[Finding] = []
|
|
103
|
+
|
|
104
|
+
for container in pod.containers:
|
|
105
|
+
findings.extend(_container_findings(pod.name, container))
|
|
106
|
+
|
|
107
|
+
if pod.phase == "Pending" and not pod.scheduling.schedulable:
|
|
108
|
+
detail = pod.scheduling.message or pod.scheduling.reason or "unschedulable"
|
|
109
|
+
findings.append(
|
|
110
|
+
Finding(Severity.CRITICAL, f"Pod {pod.name} unschedulable: {detail}")
|
|
111
|
+
)
|
|
112
|
+
elif pod.phase == "Pending":
|
|
113
|
+
findings.append(Finding(Severity.WARNING, f"Pod {pod.name} pending"))
|
|
114
|
+
elif pod.phase == "Failed":
|
|
115
|
+
findings.append(Finding(Severity.CRITICAL, f"Pod {pod.name} failed"))
|
|
116
|
+
elif (
|
|
117
|
+
pod.phase == "Running"
|
|
118
|
+
and pod.ready_containers < pod.total_containers
|
|
119
|
+
and not any(c.waiting_reason for c in pod.containers)
|
|
120
|
+
):
|
|
121
|
+
findings.append(
|
|
122
|
+
Finding(
|
|
123
|
+
Severity.WARNING,
|
|
124
|
+
f"Pod {pod.name}: {pod.ready_containers}/{pod.total_containers} containers ready",
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
return findings
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _container_findings(pod_name: str, container: ContainerDiagnostic) -> list[Finding]:
|
|
131
|
+
findings: list[Finding] = []
|
|
132
|
+
reason = container.waiting_reason
|
|
133
|
+
|
|
134
|
+
if reason == "CrashLoopBackOff":
|
|
135
|
+
findings.append(
|
|
136
|
+
Finding(
|
|
137
|
+
Severity.CRITICAL,
|
|
138
|
+
f"CrashLoopBackOff in pod {pod_name} ({container.restart_count} restarts)",
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
elif reason in _IMAGE_PULL_REASONS:
|
|
142
|
+
findings.append(
|
|
143
|
+
Finding(
|
|
144
|
+
Severity.CRITICAL,
|
|
145
|
+
f"Image pull failure ({reason}) in pod {pod_name}",
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
elif reason in _CONFIG_ERROR_REASONS:
|
|
149
|
+
detail = container.waiting_message or reason
|
|
150
|
+
findings.append(
|
|
151
|
+
Finding(
|
|
152
|
+
Severity.CRITICAL,
|
|
153
|
+
f"Container config error in pod {pod_name}: {detail}",
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
elif reason is not None:
|
|
157
|
+
findings.append(
|
|
158
|
+
Finding(
|
|
159
|
+
Severity.WARNING,
|
|
160
|
+
f"Container {container.name} in pod {pod_name} waiting: {reason}",
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if "OOMKilled" in (container.terminated_reason, container.last_terminated_reason):
|
|
165
|
+
findings.append(Finding(Severity.CRITICAL, f"OOMKilled in pod {pod_name}"))
|
|
166
|
+
elif container.terminated_reason not in (None, "Completed") and container.exit_code:
|
|
167
|
+
findings.append(
|
|
168
|
+
Finding(
|
|
169
|
+
Severity.CRITICAL,
|
|
170
|
+
f"Container {container.name} in pod {pod_name} terminated: "
|
|
171
|
+
f"{container.terminated_reason} (exit {container.exit_code})",
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if reason is None and container.restart_count >= _RESTART_WARN_THRESHOLD:
|
|
176
|
+
findings.append(
|
|
177
|
+
Finding(
|
|
178
|
+
Severity.WARNING,
|
|
179
|
+
f"Container {container.name} in pod {pod_name} "
|
|
180
|
+
f"restarted {container.restart_count} times",
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
return findings
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _event_findings(events) -> list[Finding]:
|
|
187
|
+
# The message is deliberately omitted: the WARNING EVENTS section renders it
|
|
188
|
+
# in full, so repeating it here only bloats the summary.
|
|
189
|
+
return [
|
|
190
|
+
Finding(
|
|
191
|
+
Severity.WARNING,
|
|
192
|
+
f"{event.reason} ×{event.count} on {event.kind}/{event.name}",
|
|
193
|
+
)
|
|
194
|
+
for event in events
|
|
195
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
2
|
+
from kx.refresh import ensure_exists
|
|
3
|
+
from kx.state import StateServiceProtocol
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EditCommand:
|
|
7
|
+
def __init__(self, state: StateServiceProtocol, kubectl: KubectlServiceProtocol):
|
|
8
|
+
self.state = state
|
|
9
|
+
self.kubectl = kubectl
|
|
10
|
+
|
|
11
|
+
def execute(self, index: int, extra_args: list[str] | None = None) -> None:
|
|
12
|
+
extra_args = extra_args or []
|
|
13
|
+
name, namespace, kind = self.state.fields(index)
|
|
14
|
+
rc = self.kubectl.run_interactive(
|
|
15
|
+
["edit", kind, name, "-n", namespace, *extra_args]
|
|
16
|
+
)
|
|
17
|
+
if rc != 0:
|
|
18
|
+
ensure_exists(self.kubectl, kind, name, namespace)
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
from kx.events import EventsServiceProtocol
|
|
2
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
3
|
+
from kx.refresh import ensure_exists
|
|
2
4
|
from kx.state import StateServiceProtocol
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
class EventsCommand:
|
|
6
|
-
def __init__(
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
state: StateServiceProtocol,
|
|
11
|
+
events: EventsServiceProtocol,
|
|
12
|
+
kubectl: KubectlServiceProtocol,
|
|
13
|
+
):
|
|
7
14
|
self.state = state
|
|
8
15
|
self.events = events
|
|
16
|
+
self.kubectl = kubectl
|
|
9
17
|
|
|
10
18
|
def execute(self, index: int) -> str:
|
|
11
19
|
name, namespace, kind = self.state.fields(index)
|
|
@@ -13,6 +21,9 @@ class EventsCommand:
|
|
|
13
21
|
filtered = self.events.filter(all_events, name, kind)
|
|
14
22
|
|
|
15
23
|
if not filtered:
|
|
24
|
+
# Deleted resources keep their events ~1h; only an empty result
|
|
25
|
+
# warrants checking whether the target itself is gone.
|
|
26
|
+
ensure_exists(self.kubectl, kind, name, namespace)
|
|
16
27
|
return "No events found"
|
|
17
28
|
|
|
18
29
|
output = []
|
|
@@ -2,6 +2,7 @@ import subprocess
|
|
|
2
2
|
|
|
3
3
|
from kx.kinds import Kind
|
|
4
4
|
from kx.kubectl import KubectlServiceProtocol
|
|
5
|
+
from kx.refresh import ensure_exists
|
|
5
6
|
from kx.state import StateServiceProtocol
|
|
6
7
|
|
|
7
8
|
|
|
@@ -17,8 +18,9 @@ class ExecCommand:
|
|
|
17
18
|
self.shells = shells
|
|
18
19
|
|
|
19
20
|
def execute(
|
|
20
|
-
self, index: int, cmd: list[str] | None, extra_args: list[str] =
|
|
21
|
+
self, index: int, cmd: list[str] | None, extra_args: list[str] | None = None
|
|
21
22
|
) -> None:
|
|
23
|
+
extra_args = extra_args or []
|
|
22
24
|
name, namespace, kind = self.state.fields(index)
|
|
23
25
|
if kind != Kind.Pod:
|
|
24
26
|
raise ValueError("exec is only supported for pods.")
|
|
@@ -28,6 +30,7 @@ class ExecCommand:
|
|
|
28
30
|
stderr=subprocess.DEVNULL,
|
|
29
31
|
)
|
|
30
32
|
if rc != 0:
|
|
33
|
+
ensure_exists(self.kubectl, str(Kind.Pod), name, namespace)
|
|
31
34
|
raise ValueError(f"Command failed in container (exit {rc}).")
|
|
32
35
|
else:
|
|
33
36
|
for shell in self.shells:
|
|
@@ -49,6 +52,7 @@ class ExecCommand:
|
|
|
49
52
|
["exec", "-it", name, "-n", namespace, *extra_args, "--", shell]
|
|
50
53
|
)
|
|
51
54
|
return
|
|
55
|
+
ensure_exists(self.kubectl, str(Kind.Pod), name, namespace)
|
|
52
56
|
raise ValueError(
|
|
53
57
|
"No shell found in container. Provide an explicit command: kx exec <index> -- /path/to/binary"
|
|
54
58
|
)
|