kx-cli 0.0.4__tar.gz → 0.0.6__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.6/PKG-INFO +183 -0
- kx_cli-0.0.6/README.md +162 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/pyproject.toml +3 -2
- kx_cli-0.0.6/src/kx/commands/back.py +13 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/describe.py +2 -1
- kx_cli-0.0.6/src/kx/commands/diagnostic.py +195 -0
- kx_cli-0.0.6/src/kx/commands/drop.py +9 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/edit.py +2 -1
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/exec.py +10 -5
- kx_cli-0.0.6/src/kx/commands/forward.py +13 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/get.py +2 -1
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/logs.py +2 -1
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/port_forward.py +4 -1
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/rollout.py +13 -8
- kx_cli-0.0.6/src/kx/commands/theme.py +23 -0
- kx_cli-0.0.6/src/kx/config.py +82 -0
- kx_cli-0.0.6/src/kx/console.py +654 -0
- kx_cli-0.0.6/src/kx/diagnostics.py +363 -0
- kx_cli-0.0.6/src/kx/errors.py +27 -0
- kx_cli-0.0.6/src/kx/graph.py +186 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/index.py +8 -14
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/main.py +214 -85
- kx_cli-0.0.6/src/kx/state.py +100 -0
- kx_cli-0.0.6/src/kx/themes.py +149 -0
- kx_cli-0.0.6/src/kx_cli.egg-info/PKG-INFO +183 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx_cli.egg-info/SOURCES.txt +23 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx_cli.egg-info/requires.txt +2 -1
- kx_cli-0.0.6/tests/test_back.py +36 -0
- kx_cli-0.0.6/tests/test_cli_help.py +48 -0
- kx_cli-0.0.6/tests/test_config.py +183 -0
- kx_cli-0.0.6/tests/test_console.py +622 -0
- kx_cli-0.0.6/tests/test_delete.py +34 -0
- kx_cli-0.0.6/tests/test_diagnostic.py +241 -0
- kx_cli-0.0.6/tests/test_diagnostics.py +415 -0
- kx_cli-0.0.6/tests/test_drop.py +31 -0
- kx_cli-0.0.6/tests/test_error_handling.py +94 -0
- kx_cli-0.0.6/tests/test_events.py +69 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_exec.py +14 -2
- kx_cli-0.0.6/tests/test_forward.py +36 -0
- kx_cli-0.0.6/tests/test_graph.py +233 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_index.py +3 -4
- kx_cli-0.0.6/tests/test_rollout.py +125 -0
- kx_cli-0.0.6/tests/test_state.py +372 -0
- kx_cli-0.0.6/tests/test_theme.py +40 -0
- kx_cli-0.0.6/tests/test_themes.py +36 -0
- kx_cli-0.0.6/tests/test_tree.py +59 -0
- kx_cli-0.0.4/PKG-INFO +0 -125
- kx_cli-0.0.4/README.md +0 -105
- kx_cli-0.0.4/src/kx/console.py +0 -331
- kx_cli-0.0.4/src/kx/graph.py +0 -231
- kx_cli-0.0.4/src/kx/state.py +0 -38
- kx_cli-0.0.4/src/kx_cli.egg-info/PKG-INFO +0 -125
- kx_cli-0.0.4/tests/test_console.py +0 -202
- kx_cli-0.0.4/tests/test_rollout.py +0 -69
- kx_cli-0.0.4/tests/test_state.py +0 -102
- {kx_cli-0.0.4 → kx_cli-0.0.6}/LICENSE +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/setup.cfg +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/__init__.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/__init__.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/delete.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/events.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/labels.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/namespace.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/scale.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/state.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/tree.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/commands/yaml.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/events.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/k8s.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/kinds.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/kubectl.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx/types.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx_cli.egg-info/dependency_links.txt +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx_cli.egg-info/entry_points.txt +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/src/kx_cli.egg-info/top_level.txt +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_cli_get.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_describe.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_edit.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_get.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_kinds.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_labels.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_logs.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_namespace.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_port_forward.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_scale.py +0 -0
- {kx_cli-0.0.4 → kx_cli-0.0.6}/tests/test_yaml.py +0 -0
kx_cli-0.0.6/PKG-INFO
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kx-cli
|
|
3
|
+
Version: 0.0.6
|
|
4
|
+
Summary: kubectl wrapper with index-based resource selection
|
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: typer==0.26.8
|
|
13
|
+
Requires-Dist: kubernetes
|
|
14
|
+
Requires-Dist: rich
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: ruff; extra == "dev"
|
|
17
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
<div align="center">
|
|
23
|
+
<img src="https://raw.githubusercontent.com/jzills/kx/main/assets/banner.svg" alt="kx — kubectl, indexed." width="800"/>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<br>
|
|
27
|
+
|
|
28
|
+
<div align="center">
|
|
29
|
+
|
|
30
|
+
# kubectl, indexed
|
|
31
|
+
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div align="center">
|
|
35
|
+
|
|
36
|
+
[](https://pypi.org/project/kx-cli/)
|
|
37
|
+
[](https://pypi.org/project/kx-cli/)
|
|
38
|
+
[](LICENSE)
|
|
39
|
+
[](https://github.com/jzills/kx/actions/workflows/pr.yml)
|
|
40
|
+
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
`kx` is a kubectl wrapper that adds index-based resource selection. Run `kx get <resource>` once, then reference any result by number instead of typing full resource names.
|
|
44
|
+
|
|
45
|
+
<div align="center">
|
|
46
|
+
<img src="https://raw.githubusercontent.com/jzills/kx/main/demo/demo.gif" alt="kx demo" width="800"/>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install kx-cli
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
### List resources
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
kx get <resource> [--match|-m <substring>] [kubectl flags...]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
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).
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
$ kx get pods
|
|
67
|
+
Pods · default · 3 items
|
|
68
|
+
X NAME READY STATUS RESTARTS AGE
|
|
69
|
+
1 api-7d9f4b8c6-xkp2q 1/1 Running 0 2d
|
|
70
|
+
2 worker-6c8b5f7d9-mnt4r 1/1 Running 3 5h
|
|
71
|
+
3 postgres-0 1/1 Running 0 12d
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
All subsequent commands reference resources by their `X` index from the last `kx get`.
|
|
75
|
+
|
|
76
|
+
Global flags: `--no-color` disables styled output, `--version` prints the installed version, and `--help` on any command shows usage, examples, and aliases.
|
|
77
|
+
|
|
78
|
+
### Commands
|
|
79
|
+
|
|
80
|
+
<!-- commands-table-start -->
|
|
81
|
+
| Command | Description |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `kx get <resource> [--match/-m text] [kubectl flags...]` | List resources and assign index numbers for use with other commands. |
|
|
84
|
+
| `kx describe <indexes>... [kubectl flags...]` | Show full kubectl describe output for one or more indexed resources. |
|
|
85
|
+
| `kx events <indexes>...` | Show Kubernetes events for one or more indexed resources. |
|
|
86
|
+
| `kx logs <index> [kubectl flags...]` | Stream logs for an indexed resource; aggregates across pods for Deployments, StatefulSets, DaemonSets, and Services. |
|
|
87
|
+
| `kx labels <indexes>... [--selector/-s]` | Show labels for one or more indexed resources; --selector formats output as a label selector. |
|
|
88
|
+
| `kx yaml <indexes>... [--show text]` | Print the raw YAML manifest for one or more indexed resources; --show filters to specific top-level fields. |
|
|
89
|
+
| `kx delete <indexes>... [--yes/-y]` | Delete one or more indexed resources (prompts for confirmation unless --yes). |
|
|
90
|
+
| `kx edit <index> [kubectl flags...]` | Open an indexed resource in your editor via kubectl edit. |
|
|
91
|
+
| `kx exec <index> [<cmd>...] [kubectl flags...]` | Open an interactive shell in an indexed pod (bash, falling back to sh). |
|
|
92
|
+
| `kx tree <index> [--index/-i]` | Show the ownership graph for an indexed resource; --index assigns indexes to tree nodes. |
|
|
93
|
+
| `kx rollout <action> <index>` | Run a rollout action (status, restart, pause, resume, history, undo) on a Deployment, StatefulSet, or DaemonSet. |
|
|
94
|
+
| `kx scale <index> <replicas>` | Scale an indexed Deployment, StatefulSet, or ReplicaSet to a given replica count. |
|
|
95
|
+
| `kx port-forward <index> <port> [kubectl flags...]` | Forward a local port to an indexed resource (Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Service). |
|
|
96
|
+
| `kx diagnostic <index>` | Run read-only health diagnostics on an indexed Deployment, StatefulSet, DaemonSet, or Pod; alias: kx diag. |
|
|
97
|
+
| `kx namespace <index>` | Switch to an indexed namespace; alias: kx ns (run kx get namespaces first). |
|
|
98
|
+
| `kx theme [<name>]` | List available color themes or persist a choice by name or index. |
|
|
99
|
+
| `kx state [<position>] [--all/-a]` | Show current state, jump to a history position, or list all entries with --all. |
|
|
100
|
+
| `kx drop <position>` | Remove a history entry by position (shown in kx state --all). |
|
|
101
|
+
| `kx back` | Navigate to the previous kx get result. |
|
|
102
|
+
| `kx forward` | Navigate to the next kx get result. |
|
|
103
|
+
<!-- commands-table-end -->
|
|
104
|
+
|
|
105
|
+
### Example workflow
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# list deployments, pick index 2
|
|
109
|
+
kx get deployments
|
|
110
|
+
kx describe 2
|
|
111
|
+
|
|
112
|
+
# check events on that deployment
|
|
113
|
+
kx events 2
|
|
114
|
+
|
|
115
|
+
# drill into a pod
|
|
116
|
+
kx get pods
|
|
117
|
+
kx logs 1
|
|
118
|
+
kx exec 1 # opens bash/sh
|
|
119
|
+
kx exec 1 -- env # run a specific command
|
|
120
|
+
|
|
121
|
+
# forward local port 8080 to port 80 on a service
|
|
122
|
+
kx get services
|
|
123
|
+
kx port-forward 2 8080:80
|
|
124
|
+
|
|
125
|
+
# navigate history after multiple gets
|
|
126
|
+
kx get pods
|
|
127
|
+
kx get deployments
|
|
128
|
+
kx logs 1 # logs from pod index 1
|
|
129
|
+
kx state --all # review full history
|
|
130
|
+
|
|
131
|
+
# clean up
|
|
132
|
+
kx delete 3
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## State
|
|
136
|
+
|
|
137
|
+
`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.
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
$ kx get pods # saves a new entry, cursor advances
|
|
141
|
+
$ kx get deployments # saves another entry, cursor advances
|
|
142
|
+
$ kx logs 1 # resolves index 1 from the pods result
|
|
143
|
+
$ kx state --all # lists all history entries and the current position
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Use `kx state <position>` to jump directly to any history entry, and `kx drop <position>` to remove one.
|
|
147
|
+
|
|
148
|
+
## Configuration
|
|
149
|
+
|
|
150
|
+
`kx` reads `~/.kx/config.toml`; environment variables override file settings.
|
|
151
|
+
|
|
152
|
+
| Key | Env var | Default | Description |
|
|
153
|
+
| --- | --- | --- | --- |
|
|
154
|
+
| `max_history` | `KX_MAX_HISTORY` | `10` | Number of `kx get` results kept in history. |
|
|
155
|
+
| `shells` | `KX_SHELLS` (comma-separated) | `["bash", "sh"]` | Shell candidates for `kx exec`. |
|
|
156
|
+
| `no_color` | `KX_NO_COLOR` | `false` | Disable styled output (same as `--no-color`). |
|
|
157
|
+
| `theme` | `KX_THEME` | `"github-dark"` | Color theme for all output. |
|
|
158
|
+
|
|
159
|
+
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.
|
|
160
|
+
|
|
161
|
+
### Themes
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
kx theme # list available themes (indexed) with a preview of each
|
|
165
|
+
kx theme nord # persist a theme to ~/.kx/config.toml
|
|
166
|
+
kx theme 3 # same, selecting by index from the kx theme listing
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
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).
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
python -m venv .venv
|
|
175
|
+
source .venv/bin/activate
|
|
176
|
+
pip install -e ".[dev]"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Run the CLI directly:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
python -m kx.main --help
|
|
183
|
+
```
|
kx_cli-0.0.6/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/jzills/kx/main/assets/banner.svg" alt="kx — kubectl, indexed." width="800"/>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<br>
|
|
6
|
+
|
|
7
|
+
<div align="center">
|
|
8
|
+
|
|
9
|
+
# kubectl, indexed
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div align="center">
|
|
14
|
+
|
|
15
|
+
[](https://pypi.org/project/kx-cli/)
|
|
16
|
+
[](https://pypi.org/project/kx-cli/)
|
|
17
|
+
[](LICENSE)
|
|
18
|
+
[](https://github.com/jzills/kx/actions/workflows/pr.yml)
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
`kx` is a kubectl wrapper that adds index-based resource selection. Run `kx get <resource>` once, then reference any result by number instead of typing full resource names.
|
|
23
|
+
|
|
24
|
+
<div align="center">
|
|
25
|
+
<img src="https://raw.githubusercontent.com/jzills/kx/main/demo/demo.gif" alt="kx demo" width="800"/>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install kx-cli
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### List resources
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
kx get <resource> [--match|-m <substring>] [kubectl flags...]
|
|
40
|
+
```
|
|
41
|
+
|
|
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).
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
$ kx get pods
|
|
46
|
+
Pods · default · 3 items
|
|
47
|
+
X NAME READY STATUS RESTARTS AGE
|
|
48
|
+
1 api-7d9f4b8c6-xkp2q 1/1 Running 0 2d
|
|
49
|
+
2 worker-6c8b5f7d9-mnt4r 1/1 Running 3 5h
|
|
50
|
+
3 postgres-0 1/1 Running 0 12d
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
All subsequent commands reference resources by their `X` index from the last `kx get`.
|
|
54
|
+
|
|
55
|
+
Global flags: `--no-color` disables styled output, `--version` prints the installed version, and `--help` on any command shows usage, examples, and aliases.
|
|
56
|
+
|
|
57
|
+
### Commands
|
|
58
|
+
|
|
59
|
+
<!-- commands-table-start -->
|
|
60
|
+
| Command | Description |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `kx get <resource> [--match/-m text] [kubectl flags...]` | List resources and assign index numbers for use with other commands. |
|
|
63
|
+
| `kx describe <indexes>... [kubectl flags...]` | Show full kubectl describe output for one or more indexed resources. |
|
|
64
|
+
| `kx events <indexes>...` | Show Kubernetes events for one or more indexed resources. |
|
|
65
|
+
| `kx logs <index> [kubectl flags...]` | Stream logs for an indexed resource; aggregates across pods for Deployments, StatefulSets, DaemonSets, and Services. |
|
|
66
|
+
| `kx labels <indexes>... [--selector/-s]` | Show labels for one or more indexed resources; --selector formats output as a label selector. |
|
|
67
|
+
| `kx yaml <indexes>... [--show text]` | Print the raw YAML manifest for one or more indexed resources; --show filters to specific top-level fields. |
|
|
68
|
+
| `kx delete <indexes>... [--yes/-y]` | Delete one or more indexed resources (prompts for confirmation unless --yes). |
|
|
69
|
+
| `kx edit <index> [kubectl flags...]` | Open an indexed resource in your editor via kubectl edit. |
|
|
70
|
+
| `kx exec <index> [<cmd>...] [kubectl flags...]` | Open an interactive shell in an indexed pod (bash, falling back to sh). |
|
|
71
|
+
| `kx tree <index> [--index/-i]` | Show the ownership graph for an indexed resource; --index assigns indexes to tree nodes. |
|
|
72
|
+
| `kx rollout <action> <index>` | Run a rollout action (status, restart, pause, resume, history, undo) on a Deployment, StatefulSet, or DaemonSet. |
|
|
73
|
+
| `kx scale <index> <replicas>` | Scale an indexed Deployment, StatefulSet, or ReplicaSet to a given replica count. |
|
|
74
|
+
| `kx port-forward <index> <port> [kubectl flags...]` | Forward a local port to an indexed resource (Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Service). |
|
|
75
|
+
| `kx diagnostic <index>` | Run read-only health diagnostics on an indexed Deployment, StatefulSet, DaemonSet, or Pod; alias: kx diag. |
|
|
76
|
+
| `kx namespace <index>` | Switch to an indexed namespace; alias: kx ns (run kx get namespaces first). |
|
|
77
|
+
| `kx theme [<name>]` | List available color themes or persist a choice by name or index. |
|
|
78
|
+
| `kx state [<position>] [--all/-a]` | Show current state, jump to a history position, or list all entries with --all. |
|
|
79
|
+
| `kx drop <position>` | Remove a history entry by position (shown in kx state --all). |
|
|
80
|
+
| `kx back` | Navigate to the previous kx get result. |
|
|
81
|
+
| `kx forward` | Navigate to the next kx get result. |
|
|
82
|
+
<!-- commands-table-end -->
|
|
83
|
+
|
|
84
|
+
### Example workflow
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# list deployments, pick index 2
|
|
88
|
+
kx get deployments
|
|
89
|
+
kx describe 2
|
|
90
|
+
|
|
91
|
+
# check events on that deployment
|
|
92
|
+
kx events 2
|
|
93
|
+
|
|
94
|
+
# drill into a pod
|
|
95
|
+
kx get pods
|
|
96
|
+
kx logs 1
|
|
97
|
+
kx exec 1 # opens bash/sh
|
|
98
|
+
kx exec 1 -- env # run a specific command
|
|
99
|
+
|
|
100
|
+
# forward local port 8080 to port 80 on a service
|
|
101
|
+
kx get services
|
|
102
|
+
kx port-forward 2 8080:80
|
|
103
|
+
|
|
104
|
+
# navigate history after multiple gets
|
|
105
|
+
kx get pods
|
|
106
|
+
kx get deployments
|
|
107
|
+
kx logs 1 # logs from pod index 1
|
|
108
|
+
kx state --all # review full history
|
|
109
|
+
|
|
110
|
+
# clean up
|
|
111
|
+
kx delete 3
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## State
|
|
115
|
+
|
|
116
|
+
`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.
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
$ kx get pods # saves a new entry, cursor advances
|
|
120
|
+
$ kx get deployments # saves another entry, cursor advances
|
|
121
|
+
$ kx logs 1 # resolves index 1 from the pods result
|
|
122
|
+
$ kx state --all # lists all history entries and the current position
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Use `kx state <position>` to jump directly to any history entry, and `kx drop <position>` to remove one.
|
|
126
|
+
|
|
127
|
+
## Configuration
|
|
128
|
+
|
|
129
|
+
`kx` reads `~/.kx/config.toml`; environment variables override file settings.
|
|
130
|
+
|
|
131
|
+
| Key | Env var | Default | Description |
|
|
132
|
+
| --- | --- | --- | --- |
|
|
133
|
+
| `max_history` | `KX_MAX_HISTORY` | `10` | Number of `kx get` results kept in history. |
|
|
134
|
+
| `shells` | `KX_SHELLS` (comma-separated) | `["bash", "sh"]` | Shell candidates for `kx exec`. |
|
|
135
|
+
| `no_color` | `KX_NO_COLOR` | `false` | Disable styled output (same as `--no-color`). |
|
|
136
|
+
| `theme` | `KX_THEME` | `"github-dark"` | Color theme for all output. |
|
|
137
|
+
|
|
138
|
+
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.
|
|
139
|
+
|
|
140
|
+
### Themes
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
kx theme # list available themes (indexed) with a preview of each
|
|
144
|
+
kx theme nord # persist a theme to ~/.kx/config.toml
|
|
145
|
+
kx theme 3 # same, selecting by index from the kx theme listing
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
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).
|
|
149
|
+
|
|
150
|
+
## Development
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
python -m venv .venv
|
|
154
|
+
source .venv/bin/activate
|
|
155
|
+
pip install -e ".[dev]"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Run the CLI directly:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
python -m kx.main --help
|
|
162
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "kx-cli"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.6"
|
|
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.26.
|
|
14
|
+
"typer==0.26.8",
|
|
15
15
|
"kubernetes",
|
|
16
16
|
"rich",
|
|
17
17
|
]
|
|
@@ -21,6 +21,7 @@ dev = [
|
|
|
21
21
|
"ruff",
|
|
22
22
|
"pre-commit",
|
|
23
23
|
"pytest",
|
|
24
|
+
"pytest-cov",
|
|
24
25
|
]
|
|
25
26
|
|
|
26
27
|
[project.scripts]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from dataclasses import asdict
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from kx.state import StateServiceProtocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BackCommand:
|
|
8
|
+
def __init__(self, state: StateServiceProtocol):
|
|
9
|
+
self.state = state
|
|
10
|
+
|
|
11
|
+
def execute(self) -> str:
|
|
12
|
+
state = self.state.navigate(-1)
|
|
13
|
+
return json.dumps(asdict(state), indent=2)
|
|
@@ -11,7 +11,8 @@ class DescribeCommand:
|
|
|
11
11
|
self.state = state
|
|
12
12
|
self.kubectl = kubectl
|
|
13
13
|
|
|
14
|
-
def execute(self, index: int, extra_args: list[str] =
|
|
14
|
+
def execute(self, index: int, extra_args: list[str] | None = None) -> None:
|
|
15
|
+
extra_args = extra_args or []
|
|
15
16
|
name, namespace, kind = self.state.fields(index)
|
|
16
17
|
self.kubectl.run_interactive(
|
|
17
18
|
["describe", kind, name, "-n", namespace, *extra_args]
|
|
@@ -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
|
+
]
|
|
@@ -7,6 +7,7 @@ class EditCommand:
|
|
|
7
7
|
self.state = state
|
|
8
8
|
self.kubectl = kubectl
|
|
9
9
|
|
|
10
|
-
def execute(self, index: int, extra_args: list[str] =
|
|
10
|
+
def execute(self, index: int, extra_args: list[str] | None = None) -> None:
|
|
11
|
+
extra_args = extra_args or []
|
|
11
12
|
name, namespace, kind = self.state.fields(index)
|
|
12
13
|
self.kubectl.run_interactive(["edit", kind, name, "-n", namespace, *extra_args])
|
|
@@ -4,17 +4,22 @@ from kx.kinds import Kind
|
|
|
4
4
|
from kx.kubectl import KubectlServiceProtocol
|
|
5
5
|
from kx.state import StateServiceProtocol
|
|
6
6
|
|
|
7
|
-
_SHELLS = ["bash", "sh"]
|
|
8
|
-
|
|
9
7
|
|
|
10
8
|
class ExecCommand:
|
|
11
|
-
def __init__(
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
state: StateServiceProtocol,
|
|
12
|
+
kubectl: KubectlServiceProtocol,
|
|
13
|
+
shells: tuple[str, ...] = ("bash", "sh"),
|
|
14
|
+
):
|
|
12
15
|
self.state = state
|
|
13
16
|
self.kubectl = kubectl
|
|
17
|
+
self.shells = shells
|
|
14
18
|
|
|
15
19
|
def execute(
|
|
16
|
-
self, index: int, cmd: list[str] | None, extra_args: list[str] =
|
|
20
|
+
self, index: int, cmd: list[str] | None, extra_args: list[str] | None = None
|
|
17
21
|
) -> None:
|
|
22
|
+
extra_args = extra_args or []
|
|
18
23
|
name, namespace, kind = self.state.fields(index)
|
|
19
24
|
if kind != Kind.Pod:
|
|
20
25
|
raise ValueError("exec is only supported for pods.")
|
|
@@ -26,7 +31,7 @@ class ExecCommand:
|
|
|
26
31
|
if rc != 0:
|
|
27
32
|
raise ValueError(f"Command failed in container (exit {rc}).")
|
|
28
33
|
else:
|
|
29
|
-
for shell in
|
|
34
|
+
for shell in self.shells:
|
|
30
35
|
probe_rc = self.kubectl.probe(
|
|
31
36
|
[
|
|
32
37
|
"exec",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from dataclasses import asdict
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from kx.state import StateServiceProtocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ForwardCommand:
|
|
8
|
+
def __init__(self, state: StateServiceProtocol):
|
|
9
|
+
self.state = state
|
|
10
|
+
|
|
11
|
+
def execute(self) -> str:
|
|
12
|
+
state = self.state.navigate(+1)
|
|
13
|
+
return json.dumps(asdict(state), indent=2)
|
|
@@ -28,8 +28,9 @@ class GetCommand:
|
|
|
28
28
|
self,
|
|
29
29
|
resource: str,
|
|
30
30
|
filter_term: str | None = None,
|
|
31
|
-
extra_args: list[str] =
|
|
31
|
+
extra_args: list[str] | None = None,
|
|
32
32
|
) -> str:
|
|
33
|
+
extra_args = extra_args or []
|
|
33
34
|
output = self.kubectl.run(["get", resource, *extra_args])
|
|
34
35
|
if filter_term:
|
|
35
36
|
output = self.index.filter(output, filter_term)
|
|
@@ -12,7 +12,8 @@ class LogsCommand:
|
|
|
12
12
|
self.state = state
|
|
13
13
|
self.kubectl = kubectl
|
|
14
14
|
|
|
15
|
-
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 []
|
|
16
17
|
name, namespace, kind = self.state.fields(index)
|
|
17
18
|
if kind == Kind.Pod:
|
|
18
19
|
self.kubectl.run_interactive(["logs", name, "-n", namespace, *extra_args])
|