kx-cli 0.0.2__tar.gz → 0.0.4__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.4/LICENSE +21 -0
- kx_cli-0.0.4/PKG-INFO +125 -0
- kx_cli-0.0.4/README.md +105 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/pyproject.toml +9 -2
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx/commands/delete.py +6 -1
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx/commands/describe.py +3 -1
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx/commands/events.py +5 -5
- kx_cli-0.0.4/src/kx/commands/exec.py +50 -0
- kx_cli-0.0.4/src/kx/commands/get.py +46 -0
- kx_cli-0.0.2/src/kx/commands/logs.py → kx_cli-0.0.4/src/kx/commands/labels.py +7 -6
- kx_cli-0.0.4/src/kx/commands/logs.py +42 -0
- kx_cli-0.0.2/src/kx/commands/yaml.py → kx_cli-0.0.4/src/kx/commands/namespace.py +4 -3
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx/commands/port_forward.py +11 -2
- kx_cli-0.0.4/src/kx/commands/rollout.py +30 -0
- kx_cli-0.0.4/src/kx/commands/scale.py +21 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx/commands/state.py +1 -1
- kx_cli-0.0.4/src/kx/commands/tree.py +34 -0
- kx_cli-0.0.4/src/kx/commands/yaml.py +32 -0
- kx_cli-0.0.4/src/kx/console.py +331 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx/events.py +4 -3
- kx_cli-0.0.4/src/kx/graph.py +231 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx/index.py +21 -13
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx/k8s.py +2 -1
- kx_cli-0.0.4/src/kx/kinds.py +92 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx/kubectl.py +11 -5
- kx_cli-0.0.4/src/kx/main.py +365 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx/state.py +3 -3
- kx_cli-0.0.4/src/kx/types.py +7 -0
- kx_cli-0.0.4/src/kx_cli.egg-info/PKG-INFO +125 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx_cli.egg-info/SOURCES.txt +14 -1
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx_cli.egg-info/requires.txt +2 -1
- kx_cli-0.0.4/tests/test_cli_get.py +79 -0
- kx_cli-0.0.4/tests/test_console.py +202 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/tests/test_describe.py +12 -2
- {kx_cli-0.0.2 → kx_cli-0.0.4}/tests/test_edit.py +3 -1
- {kx_cli-0.0.2 → kx_cli-0.0.4}/tests/test_exec.py +43 -7
- kx_cli-0.0.4/tests/test_get.py +135 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/tests/test_index.py +8 -6
- {kx_cli-0.0.2 → kx_cli-0.0.4}/tests/test_kinds.py +13 -1
- kx_cli-0.0.4/tests/test_labels.py +59 -0
- kx_cli-0.0.4/tests/test_logs.py +159 -0
- kx_cli-0.0.4/tests/test_namespace.py +51 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/tests/test_port_forward.py +22 -4
- kx_cli-0.0.4/tests/test_rollout.py +69 -0
- kx_cli-0.0.4/tests/test_scale.py +71 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/tests/test_state.py +26 -11
- kx_cli-0.0.4/tests/test_yaml.py +79 -0
- kx_cli-0.0.2/PKG-INFO +0 -100
- kx_cli-0.0.2/README.md +0 -87
- kx_cli-0.0.2/src/kx/commands/exec.py +0 -20
- kx_cli-0.0.2/src/kx/commands/get.py +0 -28
- kx_cli-0.0.2/src/kx/commands/tree.py +0 -16
- kx_cli-0.0.2/src/kx/graph.py +0 -91
- kx_cli-0.0.2/src/kx/kinds.py +0 -66
- kx_cli-0.0.2/src/kx/main.py +0 -142
- kx_cli-0.0.2/src/kx/types.py +0 -6
- kx_cli-0.0.2/src/kx_cli.egg-info/PKG-INFO +0 -100
- kx_cli-0.0.2/tests/test_get.py +0 -62
- kx_cli-0.0.2/tests/test_logs.py +0 -46
- {kx_cli-0.0.2 → kx_cli-0.0.4}/setup.cfg +0 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx/__init__.py +0 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx/commands/__init__.py +0 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx/commands/edit.py +0 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx_cli.egg-info/dependency_links.txt +0 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx_cli.egg-info/entry_points.txt +0 -0
- {kx_cli-0.0.2 → kx_cli-0.0.4}/src/kx_cli.egg-info/top_level.txt +0 -0
kx_cli-0.0.4/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joshua Alexander Zillwood
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
kx_cli-0.0.4/PKG-INFO
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kx-cli
|
|
3
|
+
Version: 0.0.4
|
|
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.7
|
|
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
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
<div align="center">
|
|
22
|
+
<img src="assets/banner.svg" alt="kx — kubectl, indexed." width="800"/>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<br>
|
|
26
|
+
|
|
27
|
+
<div align="center">
|
|
28
|
+
|
|
29
|
+
[](https://pypi.org/project/kx-cli/)
|
|
30
|
+
[](https://pypi.org/project/kx-cli/)
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
[](https://github.com/jzills/kx/actions/workflows/pr.yml)
|
|
33
|
+
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
`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.
|
|
37
|
+
|
|
38
|
+
<div align="center">
|
|
39
|
+
<img src="demo/demo.gif" alt="kx demo" width="800"/>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install kx-cli
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### List resources
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
kx get <resource> [--match|-m <substring>] [kubectl flags...]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
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).
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
$ kx get pods
|
|
60
|
+
X NAME READY STATUS RESTARTS AGE
|
|
61
|
+
1 api-7d9f4b8c6-xkp2q 1/1 Running 0 2d
|
|
62
|
+
2 worker-6c8b5f7d9-mnt4r 1/1 Running 3 5h
|
|
63
|
+
3 postgres-0 1/1 Running 0 12d
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
All subsequent commands reference resources by their `X` index from the last `kx get`.
|
|
67
|
+
|
|
68
|
+
### Commands
|
|
69
|
+
|
|
70
|
+
| Command | Description |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `kx get <resource> [--match\|-m <str>] [kubectl flags...]` | List resources with index numbers; optionally filter by name substring |
|
|
73
|
+
| `kx describe <index> [kubectl flags...]` | Show `kubectl describe` output for an indexed resource |
|
|
74
|
+
| `kx events <index>` | Show Kubernetes events for the resource |
|
|
75
|
+
| `kx labels <index>` | Show labels for an indexed resource |
|
|
76
|
+
| `kx logs <index> [kubectl flags...]` | Stream logs; aggregates across pods for Deployments, StatefulSets, DaemonSets, and Services |
|
|
77
|
+
| `kx yaml <index>` | Print the raw YAML manifest |
|
|
78
|
+
| `kx exec <index> [cmd] [kubectl flags...]` | Open an interactive shell in a pod (bash → sh fallback); pass a custom command with `cmd` |
|
|
79
|
+
| `kx edit <index> [kubectl flags...]` | Open the resource in your editor via `kubectl edit` |
|
|
80
|
+
| `kx delete <index> [-y]` | Delete the resource (prompts for confirmation; `-y` skips it) |
|
|
81
|
+
| `kx tree <index> [--index\|-i]` | Show the ownership graph for a resource; `--index` assigns indexes to tree nodes |
|
|
82
|
+
| `kx port-forward <index> <port> [kubectl flags...]` | Forward a local port to a resource (supports Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Service) |
|
|
83
|
+
| `kx state` | Show the current state (namespace and indexed resources from the last `kx get`) |
|
|
84
|
+
|
|
85
|
+
### Example workflow
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# list deployments, pick index 2
|
|
89
|
+
kx get deployments
|
|
90
|
+
kx describe 2
|
|
91
|
+
|
|
92
|
+
# check events on that deployment
|
|
93
|
+
kx events 2
|
|
94
|
+
|
|
95
|
+
# drill into a pod
|
|
96
|
+
kx get pods
|
|
97
|
+
kx logs 1
|
|
98
|
+
kx exec 1 # opens bash/sh
|
|
99
|
+
kx exec 1 -- env # run a specific command
|
|
100
|
+
|
|
101
|
+
# forward local port 8080 to port 80 on a service
|
|
102
|
+
kx get services
|
|
103
|
+
kx port-forward 2 8080:80
|
|
104
|
+
|
|
105
|
+
# clean up
|
|
106
|
+
kx delete 3
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## State
|
|
110
|
+
|
|
111
|
+
`kx` saves the last `get` result to `~/.kx_state.json`. Index-based commands read from this file, so switching namespaces or resource types requires a new `kx get`.
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
python -m venv .venv
|
|
117
|
+
source .venv/bin/activate
|
|
118
|
+
pip install -e ".[dev]"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Run the CLI directly:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
python -m kx.main --help
|
|
125
|
+
```
|
kx_cli-0.0.4/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="assets/banner.svg" alt="kx — kubectl, indexed." width="800"/>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<br>
|
|
6
|
+
|
|
7
|
+
<div align="center">
|
|
8
|
+
|
|
9
|
+
[](https://pypi.org/project/kx-cli/)
|
|
10
|
+
[](https://pypi.org/project/kx-cli/)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
[](https://github.com/jzills/kx/actions/workflows/pr.yml)
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
`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.
|
|
17
|
+
|
|
18
|
+
<div align="center">
|
|
19
|
+
<img src="demo/demo.gif" alt="kx demo" width="800"/>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install kx-cli
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### List resources
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
kx get <resource> [--match|-m <substring>] [kubectl flags...]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
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).
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
$ kx get pods
|
|
40
|
+
X NAME READY STATUS RESTARTS AGE
|
|
41
|
+
1 api-7d9f4b8c6-xkp2q 1/1 Running 0 2d
|
|
42
|
+
2 worker-6c8b5f7d9-mnt4r 1/1 Running 3 5h
|
|
43
|
+
3 postgres-0 1/1 Running 0 12d
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
All subsequent commands reference resources by their `X` index from the last `kx get`.
|
|
47
|
+
|
|
48
|
+
### Commands
|
|
49
|
+
|
|
50
|
+
| Command | Description |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `kx get <resource> [--match\|-m <str>] [kubectl flags...]` | List resources with index numbers; optionally filter by name substring |
|
|
53
|
+
| `kx describe <index> [kubectl flags...]` | Show `kubectl describe` output for an indexed resource |
|
|
54
|
+
| `kx events <index>` | Show Kubernetes events for the resource |
|
|
55
|
+
| `kx labels <index>` | Show labels for an indexed resource |
|
|
56
|
+
| `kx logs <index> [kubectl flags...]` | Stream logs; aggregates across pods for Deployments, StatefulSets, DaemonSets, and Services |
|
|
57
|
+
| `kx yaml <index>` | Print the raw YAML manifest |
|
|
58
|
+
| `kx exec <index> [cmd] [kubectl flags...]` | Open an interactive shell in a pod (bash → sh fallback); pass a custom command with `cmd` |
|
|
59
|
+
| `kx edit <index> [kubectl flags...]` | Open the resource in your editor via `kubectl edit` |
|
|
60
|
+
| `kx delete <index> [-y]` | Delete the resource (prompts for confirmation; `-y` skips it) |
|
|
61
|
+
| `kx tree <index> [--index\|-i]` | Show the ownership graph for a resource; `--index` assigns indexes to tree nodes |
|
|
62
|
+
| `kx port-forward <index> <port> [kubectl flags...]` | Forward a local port to a resource (supports Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Service) |
|
|
63
|
+
| `kx state` | Show the current state (namespace and indexed resources from the last `kx get`) |
|
|
64
|
+
|
|
65
|
+
### Example workflow
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# list deployments, pick index 2
|
|
69
|
+
kx get deployments
|
|
70
|
+
kx describe 2
|
|
71
|
+
|
|
72
|
+
# check events on that deployment
|
|
73
|
+
kx events 2
|
|
74
|
+
|
|
75
|
+
# drill into a pod
|
|
76
|
+
kx get pods
|
|
77
|
+
kx logs 1
|
|
78
|
+
kx exec 1 # opens bash/sh
|
|
79
|
+
kx exec 1 -- env # run a specific command
|
|
80
|
+
|
|
81
|
+
# forward local port 8080 to port 80 on a service
|
|
82
|
+
kx get services
|
|
83
|
+
kx port-forward 2 8080:80
|
|
84
|
+
|
|
85
|
+
# clean up
|
|
86
|
+
kx delete 3
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## State
|
|
90
|
+
|
|
91
|
+
`kx` saves the last `get` result to `~/.kx_state.json`. Index-based commands read from this file, so switching namespaces or resource types requires a new `kx get`.
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
python -m venv .venv
|
|
97
|
+
source .venv/bin/activate
|
|
98
|
+
pip install -e ".[dev]"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Run the CLI directly:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
python -m kx.main --help
|
|
105
|
+
```
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "kx-cli"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.4"
|
|
4
4
|
description = "kubectl wrapper with index-based resource selection"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
|
+
classifiers = [
|
|
8
|
+
"Programming Language :: Python :: 3",
|
|
9
|
+
"Programming Language :: Python :: 3.11",
|
|
10
|
+
"Programming Language :: Python :: 3.12",
|
|
11
|
+
"Programming Language :: Python :: 3.13",
|
|
12
|
+
]
|
|
7
13
|
dependencies = [
|
|
8
|
-
"typer",
|
|
14
|
+
"typer==0.26.7",
|
|
9
15
|
"kubernetes",
|
|
10
16
|
"rich",
|
|
11
17
|
]
|
|
@@ -13,6 +19,7 @@ dependencies = [
|
|
|
13
19
|
[project.optional-dependencies]
|
|
14
20
|
dev = [
|
|
15
21
|
"ruff",
|
|
22
|
+
"pre-commit",
|
|
16
23
|
"pytest",
|
|
17
24
|
]
|
|
18
25
|
|
|
@@ -4,7 +4,12 @@ from kx.types import Confirm
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class DeleteCommand:
|
|
7
|
-
def __init__(
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
state: StateServiceProtocol,
|
|
10
|
+
kubectl: KubectlServiceProtocol,
|
|
11
|
+
confirm: Confirm,
|
|
12
|
+
):
|
|
8
13
|
self.state = state
|
|
9
14
|
self.kubectl = kubectl
|
|
10
15
|
self.confirm = confirm
|
|
@@ -13,4 +13,6 @@ class DescribeCommand:
|
|
|
13
13
|
|
|
14
14
|
def execute(self, index: int, extra_args: list[str] = []) -> None:
|
|
15
15
|
name, namespace, kind = self.state.fields(index)
|
|
16
|
-
self.kubectl.run_interactive(
|
|
16
|
+
self.kubectl.run_interactive(
|
|
17
|
+
["describe", kind, name, "-n", namespace, *extra_args]
|
|
18
|
+
)
|
|
@@ -16,11 +16,11 @@ class EventsCommand:
|
|
|
16
16
|
return "No events found"
|
|
17
17
|
|
|
18
18
|
output = []
|
|
19
|
-
for
|
|
20
|
-
obj =
|
|
19
|
+
for event in filtered:
|
|
20
|
+
obj = event.involved_object
|
|
21
21
|
output.append(
|
|
22
|
-
f"{
|
|
23
|
-
f"{obj.kind:10} {
|
|
24
|
-
f"{
|
|
22
|
+
f"{event.type:8} {event.reason:30} "
|
|
23
|
+
f"{obj.kind:10} {event.metadata.creation_timestamp} "
|
|
24
|
+
f"{event.message}"
|
|
25
25
|
)
|
|
26
26
|
return "\n".join(output)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
from kx.kinds import Kind
|
|
4
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
5
|
+
from kx.state import StateServiceProtocol
|
|
6
|
+
|
|
7
|
+
_SHELLS = ["bash", "sh"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ExecCommand:
|
|
11
|
+
def __init__(self, state: StateServiceProtocol, kubectl: KubectlServiceProtocol):
|
|
12
|
+
self.state = state
|
|
13
|
+
self.kubectl = kubectl
|
|
14
|
+
|
|
15
|
+
def execute(
|
|
16
|
+
self, index: int, cmd: list[str] | None, extra_args: list[str] = []
|
|
17
|
+
) -> None:
|
|
18
|
+
name, namespace, kind = self.state.fields(index)
|
|
19
|
+
if kind != Kind.Pod:
|
|
20
|
+
raise ValueError("exec is only supported for pods.")
|
|
21
|
+
if cmd:
|
|
22
|
+
rc = self.kubectl.run_interactive(
|
|
23
|
+
["exec", "-it", name, "-n", namespace, *extra_args, "--", *cmd],
|
|
24
|
+
stderr=subprocess.DEVNULL,
|
|
25
|
+
)
|
|
26
|
+
if rc != 0:
|
|
27
|
+
raise ValueError(f"Command failed in container (exit {rc}).")
|
|
28
|
+
else:
|
|
29
|
+
for shell in _SHELLS:
|
|
30
|
+
probe_rc = self.kubectl.probe(
|
|
31
|
+
[
|
|
32
|
+
"exec",
|
|
33
|
+
name,
|
|
34
|
+
"-n",
|
|
35
|
+
namespace,
|
|
36
|
+
*extra_args,
|
|
37
|
+
"--",
|
|
38
|
+
shell,
|
|
39
|
+
"-c",
|
|
40
|
+
"exit 0",
|
|
41
|
+
]
|
|
42
|
+
)
|
|
43
|
+
if probe_rc == 0:
|
|
44
|
+
self.kubectl.run_interactive(
|
|
45
|
+
["exec", "-it", name, "-n", namespace, *extra_args, "--", shell]
|
|
46
|
+
)
|
|
47
|
+
return
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"No shell found in container. Provide an explicit command: kx exec <index> -- /path/to/binary"
|
|
50
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from kx.index import IndexServiceProtocol
|
|
2
|
+
from kx.kinds import normalize_kind
|
|
3
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
4
|
+
from kx.state import State, StateServiceProtocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _extract_namespace(extra_args: list[str]) -> str | None:
|
|
8
|
+
for index, arg in enumerate(extra_args):
|
|
9
|
+
if arg in ("-n", "--namespace") and index + 1 < len(extra_args):
|
|
10
|
+
return extra_args[index + 1]
|
|
11
|
+
if arg.startswith("--namespace="):
|
|
12
|
+
return arg.split("=", 1)[1]
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GetCommand:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
kubectl: KubectlServiceProtocol,
|
|
20
|
+
state: StateServiceProtocol,
|
|
21
|
+
index: IndexServiceProtocol,
|
|
22
|
+
):
|
|
23
|
+
self.kubectl = kubectl
|
|
24
|
+
self.state = state
|
|
25
|
+
self.index = index
|
|
26
|
+
|
|
27
|
+
def execute(
|
|
28
|
+
self,
|
|
29
|
+
resource: str,
|
|
30
|
+
filter_term: str | None = None,
|
|
31
|
+
extra_args: list[str] = [],
|
|
32
|
+
) -> str:
|
|
33
|
+
output = self.kubectl.run(["get", resource, *extra_args])
|
|
34
|
+
if filter_term:
|
|
35
|
+
output = self.index.filter(output, filter_term)
|
|
36
|
+
indexed_output, names = self.index.add(output)
|
|
37
|
+
all_namespaces = any(arg in ("-A", "--all-namespaces") for arg in extra_args)
|
|
38
|
+
if names and not all_namespaces:
|
|
39
|
+
namespace = (
|
|
40
|
+
_extract_namespace(extra_args) or self.kubectl.current_namespace()
|
|
41
|
+
)
|
|
42
|
+
kind = normalize_kind(resource)
|
|
43
|
+
self.state.save(
|
|
44
|
+
State(resources={name: kind for name in names}, namespace=namespace)
|
|
45
|
+
)
|
|
46
|
+
return indexed_output
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
import json
|
|
2
|
+
|
|
2
3
|
from kx.kubectl import KubectlServiceProtocol
|
|
3
4
|
from kx.state import StateServiceProtocol
|
|
4
5
|
|
|
5
6
|
|
|
6
|
-
class
|
|
7
|
+
class LabelsCommand:
|
|
7
8
|
def __init__(self, state: StateServiceProtocol, kubectl: KubectlServiceProtocol):
|
|
8
9
|
self.state = state
|
|
9
10
|
self.kubectl = kubectl
|
|
10
11
|
|
|
11
|
-
def execute(self, index: int
|
|
12
|
+
def execute(self, index: int) -> dict[str, str]:
|
|
12
13
|
name, namespace, kind = self.state.fields(index)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
raw = self.kubectl.run(["get", kind, name, "-n", namespace, "-o", "json"])
|
|
15
|
+
obj = json.loads(raw)
|
|
16
|
+
return obj.get("metadata", {}).get("labels") or {}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from kx.kinds import Kind
|
|
4
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
5
|
+
from kx.state import StateServiceProtocol
|
|
6
|
+
|
|
7
|
+
_AGGREGATE_KINDS = {Kind.Deployment, Kind.StatefulSet, Kind.DaemonSet, Kind.Service}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LogsCommand:
|
|
11
|
+
def __init__(self, state: StateServiceProtocol, kubectl: KubectlServiceProtocol):
|
|
12
|
+
self.state = state
|
|
13
|
+
self.kubectl = kubectl
|
|
14
|
+
|
|
15
|
+
def execute(self, index: int, extra_args: list[str] = []) -> None:
|
|
16
|
+
name, namespace, kind = self.state.fields(index)
|
|
17
|
+
if kind == Kind.Pod:
|
|
18
|
+
self.kubectl.run_interactive(["logs", name, "-n", namespace, *extra_args])
|
|
19
|
+
elif kind in _AGGREGATE_KINDS:
|
|
20
|
+
selector = self._selector(name, namespace, kind)
|
|
21
|
+
self.kubectl.run_interactive(
|
|
22
|
+
["logs", "-l", selector, "--prefix=true", "-n", namespace, *extra_args]
|
|
23
|
+
)
|
|
24
|
+
else:
|
|
25
|
+
raise ValueError(f"Logs are not supported for '{kind}'.")
|
|
26
|
+
|
|
27
|
+
def _selector(self, name: str, namespace: str, kind: str) -> str:
|
|
28
|
+
raw = self.kubectl.run(["get", kind, name, "-n", namespace, "-o", "json"])
|
|
29
|
+
obj = json.loads(raw)
|
|
30
|
+
labels = self._extract_labels(obj, kind)
|
|
31
|
+
if not labels:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"{kind}/{name} has no pod selector; cannot aggregate logs."
|
|
34
|
+
)
|
|
35
|
+
return ",".join(f"{k}={v}" for k, v in labels.items())
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _extract_labels(obj: dict, kind: str) -> dict:
|
|
39
|
+
spec = obj.get("spec", {})
|
|
40
|
+
if kind == Kind.Service:
|
|
41
|
+
return spec.get("selector") or {}
|
|
42
|
+
return (spec.get("selector") or {}).get("matchLabels") or {}
|
|
@@ -2,11 +2,12 @@ from kx.kubectl import KubectlServiceProtocol
|
|
|
2
2
|
from kx.state import StateServiceProtocol
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
class
|
|
5
|
+
class NamespaceCommand:
|
|
6
6
|
def __init__(self, state: StateServiceProtocol, kubectl: KubectlServiceProtocol):
|
|
7
7
|
self.state = state
|
|
8
8
|
self.kubectl = kubectl
|
|
9
9
|
|
|
10
10
|
def execute(self, index: int) -> str:
|
|
11
|
-
name,
|
|
12
|
-
|
|
11
|
+
name, _, _ = self.state.fields(index)
|
|
12
|
+
self.kubectl.run(["config", "set-context", "--current", f"--namespace={name}"])
|
|
13
|
+
return name
|
|
@@ -2,7 +2,14 @@ from kx.kinds import Kind
|
|
|
2
2
|
from kx.kubectl import KubectlServiceProtocol
|
|
3
3
|
from kx.state import StateServiceProtocol
|
|
4
4
|
|
|
5
|
-
_SUPPORTED_KINDS = {
|
|
5
|
+
_SUPPORTED_KINDS = {
|
|
6
|
+
Kind.Pod,
|
|
7
|
+
Kind.Deployment,
|
|
8
|
+
Kind.ReplicaSet,
|
|
9
|
+
Kind.StatefulSet,
|
|
10
|
+
Kind.DaemonSet,
|
|
11
|
+
Kind.Service,
|
|
12
|
+
}
|
|
6
13
|
|
|
7
14
|
|
|
8
15
|
class PortForwardCommand:
|
|
@@ -14,4 +21,6 @@ class PortForwardCommand:
|
|
|
14
21
|
name, namespace, kind = self.state.fields(index)
|
|
15
22
|
if kind not in _SUPPORTED_KINDS:
|
|
16
23
|
raise ValueError(f"port-forward is not supported for '{kind}'.")
|
|
17
|
-
self.kubectl.run_interactive(
|
|
24
|
+
self.kubectl.run_interactive(
|
|
25
|
+
["port-forward", f"{kind}/{name}", port, "-n", namespace, *extra_args]
|
|
26
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from kx.kinds import Kind
|
|
4
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
5
|
+
from kx.state import StateServiceProtocol
|
|
6
|
+
|
|
7
|
+
_SUPPORTED_KINDS = {Kind.Deployment, Kind.StatefulSet, Kind.DaemonSet}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RolloutAction(str, Enum):
|
|
11
|
+
status = "status"
|
|
12
|
+
restart = "restart"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RolloutCommand:
|
|
16
|
+
def __init__(self, kubectl: KubectlServiceProtocol, state: StateServiceProtocol):
|
|
17
|
+
self.kubectl = kubectl
|
|
18
|
+
self.state = state
|
|
19
|
+
|
|
20
|
+
def execute(self, index: int, restart: bool = False) -> str | None:
|
|
21
|
+
name, namespace, kind = self.state.fields(index)
|
|
22
|
+
if kind not in _SUPPORTED_KINDS:
|
|
23
|
+
raise ValueError(f"rollout is not supported for '{kind}'.")
|
|
24
|
+
if restart:
|
|
25
|
+
self.kubectl.run(["rollout", "restart", f"{kind}/{name}", "-n", namespace])
|
|
26
|
+
return f"Restarted {kind}/{name}"
|
|
27
|
+
self.kubectl.run_interactive(
|
|
28
|
+
["rollout", "status", f"{kind}/{name}", "-n", namespace]
|
|
29
|
+
)
|
|
30
|
+
return None
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from kx.kinds import Kind
|
|
2
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
3
|
+
from kx.state import StateServiceProtocol
|
|
4
|
+
|
|
5
|
+
_SUPPORTED_KINDS = {Kind.Deployment, Kind.StatefulSet, Kind.ReplicaSet}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ScaleCommand:
|
|
9
|
+
def __init__(self, kubectl: KubectlServiceProtocol, state: StateServiceProtocol):
|
|
10
|
+
self.kubectl = kubectl
|
|
11
|
+
self.state = state
|
|
12
|
+
|
|
13
|
+
def execute(self, index: int, replicas: int) -> str:
|
|
14
|
+
name, namespace, kind = self.state.fields(index)
|
|
15
|
+
if kind not in _SUPPORTED_KINDS:
|
|
16
|
+
raise ValueError(f"scale is not supported for '{kind}'.")
|
|
17
|
+
self.kubectl.run(
|
|
18
|
+
["scale", f"{kind}/{name}", f"--replicas={replicas}", "-n", namespace]
|
|
19
|
+
)
|
|
20
|
+
noun = "replica" if replicas == 1 else "replicas"
|
|
21
|
+
return f"Scaled {kind}/{name} to {replicas} {noun}"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from rich.tree import Tree
|
|
2
|
+
|
|
3
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
4
|
+
from kx.state import State, StateServiceProtocol
|
|
5
|
+
from kx.types import BuildIndexedTree, BuildTree
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TreeCommand:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
state: StateServiceProtocol,
|
|
12
|
+
kubectl: KubectlServiceProtocol,
|
|
13
|
+
build_tree: BuildTree,
|
|
14
|
+
build_indexed_tree: BuildIndexedTree,
|
|
15
|
+
):
|
|
16
|
+
self.state = state
|
|
17
|
+
self.kubectl = kubectl
|
|
18
|
+
self.build_tree = build_tree
|
|
19
|
+
self.build_indexed_tree = build_indexed_tree
|
|
20
|
+
|
|
21
|
+
def execute(self, index: int, indexed: bool = False) -> Tree:
|
|
22
|
+
name, namespace, kind = self.state.fields(index)
|
|
23
|
+
if indexed:
|
|
24
|
+
tree, resources = self.build_indexed_tree(kind, name, namespace)
|
|
25
|
+
if resources:
|
|
26
|
+
self.state.save(
|
|
27
|
+
State(
|
|
28
|
+
resources={name: kind for name, kind in resources},
|
|
29
|
+
namespace=namespace,
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
else:
|
|
33
|
+
tree = self.build_tree(kind, name, namespace)
|
|
34
|
+
return tree
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
|
|
3
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
4
|
+
from kx.state import StateServiceProtocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _find_keys(data: dict | list, keys: set[str]) -> dict:
|
|
8
|
+
result = {}
|
|
9
|
+
if isinstance(data, dict):
|
|
10
|
+
for k, v in data.items():
|
|
11
|
+
if k in keys:
|
|
12
|
+
result[k] = v
|
|
13
|
+
else:
|
|
14
|
+
result.update(_find_keys(v, keys))
|
|
15
|
+
elif isinstance(data, list):
|
|
16
|
+
for item in data:
|
|
17
|
+
result.update(_find_keys(item, keys))
|
|
18
|
+
return result
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class YamlCommand:
|
|
22
|
+
def __init__(self, state: StateServiceProtocol, kubectl: KubectlServiceProtocol):
|
|
23
|
+
self.state = state
|
|
24
|
+
self.kubectl = kubectl
|
|
25
|
+
|
|
26
|
+
def execute(self, index: int, show: list[str] | None = None) -> str:
|
|
27
|
+
name, namespace, kind = self.state.fields(index)
|
|
28
|
+
raw = self.kubectl.run(["get", kind, name, "-n", namespace, "-o", "yaml"])
|
|
29
|
+
if not show:
|
|
30
|
+
return raw
|
|
31
|
+
data = yaml.safe_load(raw)
|
|
32
|
+
return yaml.dump(_find_keys(data, set(show)), default_flow_style=False)
|