kx-cli 0.0.1__tar.gz → 0.0.2__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.2/PKG-INFO +100 -0
- kx_cli-0.0.2/README.md +87 -0
- kx_cli-0.0.2/pyproject.toml +28 -0
- kx_cli-0.0.2/src/kx/commands/delete.py +17 -0
- kx_cli-0.0.2/src/kx/commands/describe.py +16 -0
- kx_cli-0.0.2/src/kx/commands/edit.py +12 -0
- kx_cli-0.0.2/src/kx/commands/events.py +26 -0
- kx_cli-0.0.2/src/kx/commands/exec.py +20 -0
- kx_cli-0.0.2/src/kx/commands/get.py +28 -0
- kx_cli-0.0.2/src/kx/commands/logs.py +15 -0
- kx_cli-0.0.2/src/kx/commands/port_forward.py +17 -0
- kx_cli-0.0.2/src/kx/commands/state.py +13 -0
- kx_cli-0.0.2/src/kx/commands/tree.py +16 -0
- kx_cli-0.0.2/src/kx/commands/yaml.py +12 -0
- kx_cli-0.0.2/src/kx/events.py +22 -0
- {kx_cli-0.0.1 → kx_cli-0.0.2}/src/kx/graph.py +19 -17
- kx_cli-0.0.2/src/kx/index.py +91 -0
- kx_cli-0.0.2/src/kx/k8s.py +7 -0
- kx_cli-0.0.2/src/kx/kinds.py +66 -0
- kx_cli-0.0.2/src/kx/kubectl.py +33 -0
- kx_cli-0.0.2/src/kx/main.py +142 -0
- kx_cli-0.0.2/src/kx/state.py +38 -0
- kx_cli-0.0.2/src/kx/types.py +6 -0
- kx_cli-0.0.2/src/kx_cli.egg-info/PKG-INFO +100 -0
- kx_cli-0.0.2/src/kx_cli.egg-info/SOURCES.txt +39 -0
- {kx_cli-0.0.1 → kx_cli-0.0.2}/src/kx_cli.egg-info/requires.txt +4 -0
- kx_cli-0.0.2/tests/test_describe.py +36 -0
- kx_cli-0.0.2/tests/test_edit.py +36 -0
- kx_cli-0.0.2/tests/test_exec.py +61 -0
- kx_cli-0.0.2/tests/test_get.py +62 -0
- kx_cli-0.0.2/tests/test_index.py +152 -0
- kx_cli-0.0.2/tests/test_kinds.py +94 -0
- kx_cli-0.0.2/tests/test_logs.py +46 -0
- kx_cli-0.0.2/tests/test_port_forward.py +47 -0
- kx_cli-0.0.2/tests/test_state.py +87 -0
- kx_cli-0.0.1/PKG-INFO +0 -7
- kx_cli-0.0.1/pyproject.toml +0 -16
- kx_cli-0.0.1/src/kx/events.py +0 -43
- kx_cli-0.0.1/src/kx/index.py +0 -56
- kx_cli-0.0.1/src/kx/k8s.py +0 -7
- kx_cli-0.0.1/src/kx/kubectl.py +0 -16
- kx_cli-0.0.1/src/kx/main.py +0 -166
- kx_cli-0.0.1/src/kx/state.py +0 -23
- kx_cli-0.0.1/src/kx_cli.egg-info/PKG-INFO +0 -7
- kx_cli-0.0.1/src/kx_cli.egg-info/SOURCES.txt +0 -16
- {kx_cli-0.0.1 → kx_cli-0.0.2}/setup.cfg +0 -0
- {kx_cli-0.0.1 → kx_cli-0.0.2}/src/kx/__init__.py +0 -0
- /kx_cli-0.0.1/README.md → /kx_cli-0.0.2/src/kx/commands/__init__.py +0 -0
- {kx_cli-0.0.1 → kx_cli-0.0.2}/src/kx_cli.egg-info/dependency_links.txt +0 -0
- {kx_cli-0.0.1 → kx_cli-0.0.2}/src/kx_cli.egg-info/entry_points.txt +0 -0
- {kx_cli-0.0.1 → kx_cli-0.0.2}/src/kx_cli.egg-info/top_level.txt +0 -0
kx_cli-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kx-cli
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: kubectl wrapper with index-based resource selection
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: typer
|
|
8
|
+
Requires-Dist: kubernetes
|
|
9
|
+
Requires-Dist: rich
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: ruff; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# kx
|
|
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
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install kx-cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### List resources
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
kx get <resource> [-n <namespace>]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Fetches resources and assigns index numbers. Omitting `-n` uses the current context namespace.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
$ kx get pods
|
|
36
|
+
X NAME READY STATUS RESTARTS AGE
|
|
37
|
+
1 api-7d9f4b8c6-xkp2q 1/1 Running 0 2d
|
|
38
|
+
2 worker-6c8b5f7d9-mnt4r 1/1 Running 3 5h
|
|
39
|
+
3 postgres-0 1/1 Running 0 12d
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
All subsequent commands reference resources by their `X` index from the last `kx get`.
|
|
43
|
+
|
|
44
|
+
### Commands
|
|
45
|
+
|
|
46
|
+
| Command | Description |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `kx get <resource> [-n ns]` | List resources with index numbers |
|
|
49
|
+
| `kx describe <index>` | Show `kubectl describe` output for an indexed resource |
|
|
50
|
+
| `kx events <index>` | Show Kubernetes events for the resource |
|
|
51
|
+
| `kx logs <index>` | Stream logs for a pod |
|
|
52
|
+
| `kx yaml <index>` | Print the raw YAML manifest |
|
|
53
|
+
| `kx exec <index> [cmd]` | Open an interactive shell in a pod (bash → sh fallback); pass a custom command with `cmd` |
|
|
54
|
+
| `kx edit <index>` | Open the resource in your editor via `kubectl edit` |
|
|
55
|
+
| `kx delete <index> [-y]` | Delete the resource (prompts for confirmation; `-y` skips it) |
|
|
56
|
+
| `kx tree <index>` | Show the ownership graph for a resource |
|
|
57
|
+
| `kx port-forward <index> <port>` | Forward a local port to a resource (supports Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Service) |
|
|
58
|
+
| `kx state` | Show the current state (last `kx get` result) as JSON |
|
|
59
|
+
|
|
60
|
+
### Example workflow
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# list deployments, pick index 2
|
|
64
|
+
kx get deployments
|
|
65
|
+
kx describe 2
|
|
66
|
+
|
|
67
|
+
# check events on that deployment
|
|
68
|
+
kx events 2
|
|
69
|
+
|
|
70
|
+
# drill into a pod
|
|
71
|
+
kx get pods
|
|
72
|
+
kx logs 1
|
|
73
|
+
kx exec 1 # opens bash/sh
|
|
74
|
+
kx exec 1 -- env # run a specific command
|
|
75
|
+
|
|
76
|
+
# forward local port 8080 to port 80 on a service
|
|
77
|
+
kx get services
|
|
78
|
+
kx port-forward 2 8080:80
|
|
79
|
+
|
|
80
|
+
# clean up
|
|
81
|
+
kx delete 3
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## State
|
|
85
|
+
|
|
86
|
+
`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`.
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
python -m venv .venv
|
|
92
|
+
source .venv/bin/activate
|
|
93
|
+
pip install -e ".[dev]"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Run the CLI directly:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python -m kx.main --help
|
|
100
|
+
```
|
kx_cli-0.0.2/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# kx
|
|
2
|
+
|
|
3
|
+
`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.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install kx-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### List resources
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
kx get <resource> [-n <namespace>]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Fetches resources and assigns index numbers. Omitting `-n` uses the current context namespace.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
$ kx get pods
|
|
23
|
+
X NAME READY STATUS RESTARTS AGE
|
|
24
|
+
1 api-7d9f4b8c6-xkp2q 1/1 Running 0 2d
|
|
25
|
+
2 worker-6c8b5f7d9-mnt4r 1/1 Running 3 5h
|
|
26
|
+
3 postgres-0 1/1 Running 0 12d
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
All subsequent commands reference resources by their `X` index from the last `kx get`.
|
|
30
|
+
|
|
31
|
+
### Commands
|
|
32
|
+
|
|
33
|
+
| Command | Description |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `kx get <resource> [-n ns]` | List resources with index numbers |
|
|
36
|
+
| `kx describe <index>` | Show `kubectl describe` output for an indexed resource |
|
|
37
|
+
| `kx events <index>` | Show Kubernetes events for the resource |
|
|
38
|
+
| `kx logs <index>` | Stream logs for a pod |
|
|
39
|
+
| `kx yaml <index>` | Print the raw YAML manifest |
|
|
40
|
+
| `kx exec <index> [cmd]` | Open an interactive shell in a pod (bash → sh fallback); pass a custom command with `cmd` |
|
|
41
|
+
| `kx edit <index>` | Open the resource in your editor via `kubectl edit` |
|
|
42
|
+
| `kx delete <index> [-y]` | Delete the resource (prompts for confirmation; `-y` skips it) |
|
|
43
|
+
| `kx tree <index>` | Show the ownership graph for a resource |
|
|
44
|
+
| `kx port-forward <index> <port>` | Forward a local port to a resource (supports Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Service) |
|
|
45
|
+
| `kx state` | Show the current state (last `kx get` result) as JSON |
|
|
46
|
+
|
|
47
|
+
### Example workflow
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# list deployments, pick index 2
|
|
51
|
+
kx get deployments
|
|
52
|
+
kx describe 2
|
|
53
|
+
|
|
54
|
+
# check events on that deployment
|
|
55
|
+
kx events 2
|
|
56
|
+
|
|
57
|
+
# drill into a pod
|
|
58
|
+
kx get pods
|
|
59
|
+
kx logs 1
|
|
60
|
+
kx exec 1 # opens bash/sh
|
|
61
|
+
kx exec 1 -- env # run a specific command
|
|
62
|
+
|
|
63
|
+
# forward local port 8080 to port 80 on a service
|
|
64
|
+
kx get services
|
|
65
|
+
kx port-forward 2 8080:80
|
|
66
|
+
|
|
67
|
+
# clean up
|
|
68
|
+
kx delete 3
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## State
|
|
72
|
+
|
|
73
|
+
`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`.
|
|
74
|
+
|
|
75
|
+
## Development
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python -m venv .venv
|
|
79
|
+
source .venv/bin/activate
|
|
80
|
+
pip install -e ".[dev]"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Run the CLI directly:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
python -m kx.main --help
|
|
87
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "kx-cli"
|
|
3
|
+
version = "0.0.2"
|
|
4
|
+
description = "kubectl wrapper with index-based resource selection"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"typer",
|
|
9
|
+
"kubernetes",
|
|
10
|
+
"rich",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
dev = [
|
|
15
|
+
"ruff",
|
|
16
|
+
"pytest",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
kx = "kx.main:app"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["setuptools>=61"]
|
|
24
|
+
build-backend = "setuptools.build_meta"
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
testpaths = ["tests"]
|
|
28
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
2
|
+
from kx.state import StateServiceProtocol
|
|
3
|
+
from kx.types import Confirm
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DeleteCommand:
|
|
7
|
+
def __init__(self, state: StateServiceProtocol, kubectl: KubectlServiceProtocol, confirm: Confirm):
|
|
8
|
+
self.state = state
|
|
9
|
+
self.kubectl = kubectl
|
|
10
|
+
self.confirm = confirm
|
|
11
|
+
|
|
12
|
+
def execute(self, index: int, yes: bool) -> str:
|
|
13
|
+
name, namespace, kind = self.state.fields(index)
|
|
14
|
+
if not yes:
|
|
15
|
+
self.confirm(f"Delete {kind}/{name} in {namespace}?")
|
|
16
|
+
self.kubectl.run(["delete", kind, name, "-n", namespace])
|
|
17
|
+
return f"Deleted {kind}/{name}"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
2
|
+
from kx.state import StateServiceProtocol
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DescribeCommand:
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
state: StateServiceProtocol,
|
|
9
|
+
kubectl: KubectlServiceProtocol,
|
|
10
|
+
):
|
|
11
|
+
self.state = state
|
|
12
|
+
self.kubectl = kubectl
|
|
13
|
+
|
|
14
|
+
def execute(self, index: int, extra_args: list[str] = []) -> None:
|
|
15
|
+
name, namespace, kind = self.state.fields(index)
|
|
16
|
+
self.kubectl.run_interactive(["describe", kind, name, "-n", namespace, *extra_args])
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
2
|
+
from kx.state import StateServiceProtocol
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class EditCommand:
|
|
6
|
+
def __init__(self, state: StateServiceProtocol, kubectl: KubectlServiceProtocol):
|
|
7
|
+
self.state = state
|
|
8
|
+
self.kubectl = kubectl
|
|
9
|
+
|
|
10
|
+
def execute(self, index: int, extra_args: list[str] = []) -> None:
|
|
11
|
+
name, namespace, kind = self.state.fields(index)
|
|
12
|
+
self.kubectl.run_interactive(["edit", kind, name, "-n", namespace, *extra_args])
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from kx.events import EventsServiceProtocol
|
|
2
|
+
from kx.state import StateServiceProtocol
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class EventsCommand:
|
|
6
|
+
def __init__(self, state: StateServiceProtocol, events: EventsServiceProtocol):
|
|
7
|
+
self.state = state
|
|
8
|
+
self.events = events
|
|
9
|
+
|
|
10
|
+
def execute(self, index: int) -> str:
|
|
11
|
+
name, namespace, kind = self.state.fields(index)
|
|
12
|
+
all_events = self.events.get(namespace)
|
|
13
|
+
filtered = self.events.filter(all_events, name, kind)
|
|
14
|
+
|
|
15
|
+
if not filtered:
|
|
16
|
+
return "No events found"
|
|
17
|
+
|
|
18
|
+
output = []
|
|
19
|
+
for e in filtered:
|
|
20
|
+
obj = e.involved_object
|
|
21
|
+
output.append(
|
|
22
|
+
f"{e.type:8} {e.reason:30} "
|
|
23
|
+
f"{obj.kind:10} {e.metadata.creation_timestamp} "
|
|
24
|
+
f"{e.message}"
|
|
25
|
+
)
|
|
26
|
+
return "\n".join(output)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from kx.kinds import Kind
|
|
2
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
3
|
+
from kx.state import StateServiceProtocol
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ExecCommand:
|
|
7
|
+
def __init__(self, state: StateServiceProtocol, kubectl: KubectlServiceProtocol):
|
|
8
|
+
self.state = state
|
|
9
|
+
self.kubectl = kubectl
|
|
10
|
+
|
|
11
|
+
def execute(self, index: int, cmd: list[str] | None, extra_args: list[str] = []) -> None:
|
|
12
|
+
name, namespace, kind = self.state.fields(index)
|
|
13
|
+
if kind != Kind.Pod:
|
|
14
|
+
raise ValueError("exec is only supported for pods.")
|
|
15
|
+
if cmd:
|
|
16
|
+
self.kubectl.run_interactive(["exec", "-it", name, "-n", namespace, *extra_args, "--", *cmd])
|
|
17
|
+
else:
|
|
18
|
+
rc = self.kubectl.run_interactive(["exec", "-it", name, "-n", namespace, *extra_args, "--", "bash"])
|
|
19
|
+
if rc != 0:
|
|
20
|
+
self.kubectl.run_interactive(["exec", "-it", name, "-n", namespace, *extra_args, "--", "sh"])
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
class GetCommand:
|
|
8
|
+
def __init__(
|
|
9
|
+
self,
|
|
10
|
+
kubectl: KubectlServiceProtocol,
|
|
11
|
+
state: StateServiceProtocol,
|
|
12
|
+
index: IndexServiceProtocol,
|
|
13
|
+
):
|
|
14
|
+
self.kubectl = kubectl
|
|
15
|
+
self.state = state
|
|
16
|
+
self.index = index
|
|
17
|
+
|
|
18
|
+
def execute(self, resource: str, namespace: str, filter_term: str | None = None, extra_args: list[str] = []) -> str:
|
|
19
|
+
if namespace is None:
|
|
20
|
+
namespace = self.kubectl.current_namespace()
|
|
21
|
+
|
|
22
|
+
output = self.kubectl.run(["get", resource, "-n", namespace, *extra_args])
|
|
23
|
+
if filter_term:
|
|
24
|
+
output = self.index.filter(output, filter_term)
|
|
25
|
+
indexed_output, names = self.index.add(output)
|
|
26
|
+
if names:
|
|
27
|
+
self.state.save(State(kind=str(normalize_kind(resource)), names=names, namespace=namespace))
|
|
28
|
+
return indexed_output
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from kx.kinds import Kind
|
|
2
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
3
|
+
from kx.state import StateServiceProtocol
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LogsCommand:
|
|
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:
|
|
12
|
+
name, namespace, kind = self.state.fields(index)
|
|
13
|
+
if kind != Kind.Pod:
|
|
14
|
+
raise ValueError("Logs are only supported on pods.")
|
|
15
|
+
self.kubectl.run_interactive(["logs", name, "-n", namespace, *extra_args])
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from kx.kinds import Kind
|
|
2
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
3
|
+
from kx.state import StateServiceProtocol
|
|
4
|
+
|
|
5
|
+
_SUPPORTED_KINDS = {Kind.Pod, Kind.Deployment, Kind.ReplicaSet, Kind.StatefulSet, Kind.DaemonSet, Kind.Service}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PortForwardCommand:
|
|
9
|
+
def __init__(self, kubectl: KubectlServiceProtocol, state: StateServiceProtocol):
|
|
10
|
+
self.kubectl = kubectl
|
|
11
|
+
self.state = state
|
|
12
|
+
|
|
13
|
+
def execute(self, index: int, port: str, extra_args: list[str] = []) -> None:
|
|
14
|
+
name, namespace, kind = self.state.fields(index)
|
|
15
|
+
if kind not in _SUPPORTED_KINDS:
|
|
16
|
+
raise ValueError(f"port-forward is not supported for '{kind}'.")
|
|
17
|
+
self.kubectl.run_interactive(["port-forward", f"{kind}/{name}", port, "-n", namespace, *extra_args])
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from dataclasses import asdict
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from kx.state import State, StateServiceProtocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StateCommand:
|
|
8
|
+
def __init__(self, state: StateServiceProtocol):
|
|
9
|
+
self.state = state
|
|
10
|
+
|
|
11
|
+
def execute(self) -> State:
|
|
12
|
+
state = self.state.load()
|
|
13
|
+
return json.dumps(asdict(state), indent=2)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from rich.tree import Tree
|
|
2
|
+
|
|
3
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
4
|
+
from kx.state import StateServiceProtocol
|
|
5
|
+
from kx.types import BuildTree
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TreeCommand:
|
|
9
|
+
def __init__(self, state: StateServiceProtocol, kubectl: KubectlServiceProtocol, build_tree: BuildTree):
|
|
10
|
+
self.state = state
|
|
11
|
+
self.kubectl = kubectl
|
|
12
|
+
self.build_tree = build_tree
|
|
13
|
+
|
|
14
|
+
def execute(self, index: int) -> Tree:
|
|
15
|
+
name, namespace, kind = self.state.fields(index)
|
|
16
|
+
return self.build_tree(kind, name, namespace)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
2
|
+
from kx.state import StateServiceProtocol
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class YamlCommand:
|
|
6
|
+
def __init__(self, state: StateServiceProtocol, kubectl: KubectlServiceProtocol):
|
|
7
|
+
self.state = state
|
|
8
|
+
self.kubectl = kubectl
|
|
9
|
+
|
|
10
|
+
def execute(self, index: int) -> str:
|
|
11
|
+
name, namespace, kind = self.state.fields(index)
|
|
12
|
+
return self.kubectl.run(["get", kind, name, "-n", namespace, "-o", "yaml"])
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from kubernetes import client
|
|
2
|
+
from typing import Protocol
|
|
3
|
+
|
|
4
|
+
from kx.k8s import load_config
|
|
5
|
+
|
|
6
|
+
class EventsServiceProtocol(Protocol):
|
|
7
|
+
def get(self, namespace: str) -> list: ...
|
|
8
|
+
def filter(self, events: list, name: str, kind: str) -> list: ...
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EventsService:
|
|
12
|
+
def get(self, namespace: str) -> list:
|
|
13
|
+
load_config()
|
|
14
|
+
v1 = client.CoreV1Api()
|
|
15
|
+
return v1.list_namespaced_event(namespace=namespace).items
|
|
16
|
+
|
|
17
|
+
def filter(self, events: list, name: str, kind: str) -> list:
|
|
18
|
+
return [
|
|
19
|
+
e for e in events
|
|
20
|
+
if e.involved_object.name == name
|
|
21
|
+
and e.involved_object.kind == kind
|
|
22
|
+
]
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from kubernetes import client
|
|
2
2
|
from rich.tree import Tree
|
|
3
3
|
|
|
4
|
-
from kx.k8s import
|
|
4
|
+
from kx.k8s import load_config
|
|
5
|
+
from kx.kinds import Kind
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def build_tree(kind: str, name: str, namespace: str) -> Tree:
|
|
8
|
-
|
|
9
|
+
load_config()
|
|
9
10
|
root = Tree(f"[bold]{kind}/{name}[/bold]")
|
|
10
11
|
|
|
11
12
|
apps = client.AppsV1Api()
|
|
@@ -14,21 +15,22 @@ def build_tree(kind: str, name: str, namespace: str) -> Tree:
|
|
|
14
15
|
|
|
15
16
|
pods = core.list_namespaced_pod(namespace).items
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
match kind:
|
|
19
|
+
case Kind.Deployment:
|
|
20
|
+
_tree_deployment(name, namespace, root, apps, pods)
|
|
21
|
+
case Kind.ReplicaSet:
|
|
22
|
+
_tree_replica_set(name, namespace, root, apps, pods)
|
|
23
|
+
case Kind.StatefulSet:
|
|
24
|
+
_tree_stateful_set(name, namespace, root, apps, pods)
|
|
25
|
+
case Kind.DaemonSet:
|
|
26
|
+
_tree_daemon_set(name, namespace, root, apps, pods)
|
|
27
|
+
case Kind.CronJob:
|
|
28
|
+
_tree_cron_job(name, namespace, root, batch, pods)
|
|
29
|
+
case Kind.Pod:
|
|
30
|
+
pod = core.read_namespaced_pod(name, namespace)
|
|
31
|
+
_add_containers(pod, root)
|
|
32
|
+
case _:
|
|
33
|
+
root.add(f"[dim](no ownership graph for {kind})[/dim]")
|
|
32
34
|
|
|
33
35
|
return root
|
|
34
36
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import TYPE_CHECKING, Protocol
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from kx.state import State
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resolve_index(state, index: int) -> str:
|
|
11
|
+
if index < 1:
|
|
12
|
+
typer.echo("Invalid index")
|
|
13
|
+
raise typer.Exit(1)
|
|
14
|
+
try:
|
|
15
|
+
return state.names[index - 1]
|
|
16
|
+
except IndexError:
|
|
17
|
+
typer.echo("Invalid index")
|
|
18
|
+
raise typer.Exit(1)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_output(output: str) -> tuple[list[str], list[list[str]], int]:
|
|
22
|
+
lines = output.splitlines()
|
|
23
|
+
if not lines:
|
|
24
|
+
return [], [], 0
|
|
25
|
+
|
|
26
|
+
header = lines[0]
|
|
27
|
+
spans = [(m.start(), m.end()) for m in re.finditer(r"\S+\s*", header)]
|
|
28
|
+
headers = [header[s:e].strip() for s, e in spans]
|
|
29
|
+
if "NAME" not in headers:
|
|
30
|
+
return [], [], 0
|
|
31
|
+
name_idx = headers.index("NAME")
|
|
32
|
+
|
|
33
|
+
rows = []
|
|
34
|
+
for r in lines[1:]:
|
|
35
|
+
if not r.strip():
|
|
36
|
+
continue
|
|
37
|
+
cols = [r[s:e].strip() for s, e in spans]
|
|
38
|
+
if len(cols) <= name_idx:
|
|
39
|
+
continue
|
|
40
|
+
rows.append(cols)
|
|
41
|
+
|
|
42
|
+
return headers, rows, name_idx
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class IndexServiceProtocol(Protocol):
|
|
46
|
+
def add(self, output: str) -> tuple[str, list[str]]: ...
|
|
47
|
+
def filter(self, output: str, term: str) -> str: ...
|
|
48
|
+
def resolve(self, state: "State", index: int) -> str: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class IndexService:
|
|
52
|
+
def add(self, output: str) -> tuple[str, list[str]]:
|
|
53
|
+
headers, rows, name_idx = _parse_output(output)
|
|
54
|
+
if not headers:
|
|
55
|
+
return output, []
|
|
56
|
+
|
|
57
|
+
names = [r[name_idx] for r in rows]
|
|
58
|
+
|
|
59
|
+
headers = ["X"] + headers
|
|
60
|
+
rows = [[str(i + 1)] + r for i, r in enumerate(rows)]
|
|
61
|
+
|
|
62
|
+
all_rows = [headers] + rows
|
|
63
|
+
cols = list(zip(*all_rows))
|
|
64
|
+
widths = [max(len(cell) for cell in col) for col in cols]
|
|
65
|
+
|
|
66
|
+
def fmt(row: list[str]) -> str:
|
|
67
|
+
return " ".join(cell.ljust(widths[i]) for i, cell in enumerate(row))
|
|
68
|
+
|
|
69
|
+
return "\n".join(fmt(r) for r in all_rows), names
|
|
70
|
+
|
|
71
|
+
def filter(self, output: str, term: str) -> str:
|
|
72
|
+
headers, rows, name_idx = _parse_output(output)
|
|
73
|
+
if not headers:
|
|
74
|
+
return output
|
|
75
|
+
|
|
76
|
+
filtered = [r for r in rows if term.lower() in r[name_idx].lower()]
|
|
77
|
+
|
|
78
|
+
all_rows = [headers] + filtered
|
|
79
|
+
if len(all_rows) == 1:
|
|
80
|
+
return output.splitlines()[0]
|
|
81
|
+
|
|
82
|
+
cols = list(zip(*all_rows))
|
|
83
|
+
widths = [max(len(cell) for cell in col) for col in cols]
|
|
84
|
+
|
|
85
|
+
def fmt(row: list[str]) -> str:
|
|
86
|
+
return " ".join(cell.ljust(widths[i]) for i, cell in enumerate(row))
|
|
87
|
+
|
|
88
|
+
return "\n".join(fmt(r) for r in all_rows)
|
|
89
|
+
|
|
90
|
+
def resolve(self, state: "State", index: int) -> str:
|
|
91
|
+
return resolve_index(state, index)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Kind(StrEnum):
|
|
5
|
+
Pod = "Pod"
|
|
6
|
+
Deployment = "Deployment"
|
|
7
|
+
ReplicaSet = "ReplicaSet"
|
|
8
|
+
StatefulSet = "StatefulSet"
|
|
9
|
+
DaemonSet = "DaemonSet"
|
|
10
|
+
CronJob = "CronJob"
|
|
11
|
+
Service = "Service"
|
|
12
|
+
HorizontalPodAutoscaler = "HorizontalPodAutoscaler"
|
|
13
|
+
Ingress = "Ingress"
|
|
14
|
+
ConfigMap = "ConfigMap"
|
|
15
|
+
Secret = "Secret"
|
|
16
|
+
Job = "Job"
|
|
17
|
+
PersistentVolumeClaim = "PersistentVolumeClaim"
|
|
18
|
+
Node = "Node"
|
|
19
|
+
Namespace = "Namespace"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_KIND_MAP: dict[str, Kind] = {
|
|
23
|
+
"po": Kind.Pod,
|
|
24
|
+
"pod": Kind.Pod,
|
|
25
|
+
"pods": Kind.Pod,
|
|
26
|
+
"deployment": Kind.Deployment,
|
|
27
|
+
"deployments": Kind.Deployment,
|
|
28
|
+
"deploy": Kind.Deployment,
|
|
29
|
+
"replicaset": Kind.ReplicaSet,
|
|
30
|
+
"replicasets": Kind.ReplicaSet,
|
|
31
|
+
"rs": Kind.ReplicaSet,
|
|
32
|
+
"statefulset": Kind.StatefulSet,
|
|
33
|
+
"statefulsets": Kind.StatefulSet,
|
|
34
|
+
"sts": Kind.StatefulSet,
|
|
35
|
+
"daemonset": Kind.DaemonSet,
|
|
36
|
+
"daemonsets": Kind.DaemonSet,
|
|
37
|
+
"ds": Kind.DaemonSet,
|
|
38
|
+
"hpa": Kind.HorizontalPodAutoscaler,
|
|
39
|
+
"horizontalpodautoscaler": Kind.HorizontalPodAutoscaler,
|
|
40
|
+
"horizontalpodautoscalers": Kind.HorizontalPodAutoscaler,
|
|
41
|
+
"service": Kind.Service,
|
|
42
|
+
"services": Kind.Service,
|
|
43
|
+
"svc": Kind.Service,
|
|
44
|
+
"ingress": Kind.Ingress,
|
|
45
|
+
"ingresses": Kind.Ingress,
|
|
46
|
+
"configmap": Kind.ConfigMap,
|
|
47
|
+
"configmaps": Kind.ConfigMap,
|
|
48
|
+
"cm": Kind.ConfigMap,
|
|
49
|
+
"secret": Kind.Secret,
|
|
50
|
+
"secrets": Kind.Secret,
|
|
51
|
+
"job": Kind.Job,
|
|
52
|
+
"jobs": Kind.Job,
|
|
53
|
+
"cronjob": Kind.CronJob,
|
|
54
|
+
"cronjobs": Kind.CronJob,
|
|
55
|
+
"pvc": Kind.PersistentVolumeClaim,
|
|
56
|
+
"persistentvolumeclaim": Kind.PersistentVolumeClaim,
|
|
57
|
+
"persistentvolumeclaims": Kind.PersistentVolumeClaim,
|
|
58
|
+
"node": Kind.Node,
|
|
59
|
+
"nodes": Kind.Node,
|
|
60
|
+
"namespace": Kind.Namespace,
|
|
61
|
+
"namespaces": Kind.Namespace,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def normalize_kind(resource_type: str) -> Kind | str:
|
|
66
|
+
return _KIND_MAP.get(resource_type.lower(), resource_type)
|