kx-cli 0.0.3__tar.gz → 0.0.5__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.
Files changed (69) hide show
  1. kx_cli-0.0.5/PKG-INFO +154 -0
  2. kx_cli-0.0.5/README.md +134 -0
  3. {kx_cli-0.0.3 → kx_cli-0.0.5}/pyproject.toml +8 -3
  4. kx_cli-0.0.5/src/kx/commands/back.py +13 -0
  5. kx_cli-0.0.5/src/kx/commands/drop.py +9 -0
  6. kx_cli-0.0.5/src/kx/commands/exec.py +54 -0
  7. kx_cli-0.0.5/src/kx/commands/forward.py +13 -0
  8. kx_cli-0.0.5/src/kx/commands/labels.py +16 -0
  9. kx_cli-0.0.3/src/kx/commands/yaml.py → kx_cli-0.0.5/src/kx/commands/namespace.py +4 -3
  10. kx_cli-0.0.5/src/kx/commands/rollout.py +35 -0
  11. kx_cli-0.0.5/src/kx/commands/scale.py +21 -0
  12. kx_cli-0.0.5/src/kx/commands/yaml.py +32 -0
  13. kx_cli-0.0.5/src/kx/config.py +38 -0
  14. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/console.py +102 -15
  15. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/kinds.py +2 -2
  16. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/kubectl.py +8 -3
  17. kx_cli-0.0.5/src/kx/main.py +418 -0
  18. kx_cli-0.0.5/src/kx/state.py +100 -0
  19. kx_cli-0.0.5/src/kx_cli.egg-info/PKG-INFO +154 -0
  20. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx_cli.egg-info/SOURCES.txt +18 -1
  21. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx_cli.egg-info/requires.txt +1 -2
  22. kx_cli-0.0.5/tests/test_back.py +36 -0
  23. kx_cli-0.0.5/tests/test_config.py +87 -0
  24. {kx_cli-0.0.3 → kx_cli-0.0.5}/tests/test_console.py +51 -7
  25. kx_cli-0.0.5/tests/test_drop.py +31 -0
  26. {kx_cli-0.0.3 → kx_cli-0.0.5}/tests/test_exec.py +56 -8
  27. kx_cli-0.0.5/tests/test_forward.py +36 -0
  28. {kx_cli-0.0.3 → kx_cli-0.0.5}/tests/test_kinds.py +13 -1
  29. kx_cli-0.0.5/tests/test_labels.py +59 -0
  30. kx_cli-0.0.5/tests/test_namespace.py +51 -0
  31. kx_cli-0.0.5/tests/test_rollout.py +125 -0
  32. kx_cli-0.0.5/tests/test_scale.py +71 -0
  33. kx_cli-0.0.5/tests/test_state.py +373 -0
  34. kx_cli-0.0.5/tests/test_yaml.py +79 -0
  35. kx_cli-0.0.3/PKG-INFO +0 -117
  36. kx_cli-0.0.3/README.md +0 -100
  37. kx_cli-0.0.3/src/kx/commands/exec.py +0 -28
  38. kx_cli-0.0.3/src/kx/main.py +0 -252
  39. kx_cli-0.0.3/src/kx/state.py +0 -38
  40. kx_cli-0.0.3/src/kx_cli.egg-info/PKG-INFO +0 -117
  41. kx_cli-0.0.3/tests/test_state.py +0 -102
  42. {kx_cli-0.0.3 → kx_cli-0.0.5}/LICENSE +0 -0
  43. {kx_cli-0.0.3 → kx_cli-0.0.5}/setup.cfg +0 -0
  44. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/__init__.py +0 -0
  45. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/commands/__init__.py +0 -0
  46. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/commands/delete.py +0 -0
  47. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/commands/describe.py +0 -0
  48. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/commands/edit.py +0 -0
  49. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/commands/events.py +0 -0
  50. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/commands/get.py +0 -0
  51. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/commands/logs.py +0 -0
  52. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/commands/port_forward.py +0 -0
  53. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/commands/state.py +0 -0
  54. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/commands/tree.py +0 -0
  55. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/events.py +0 -0
  56. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/graph.py +0 -0
  57. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/index.py +0 -0
  58. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/k8s.py +0 -0
  59. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx/types.py +0 -0
  60. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx_cli.egg-info/dependency_links.txt +0 -0
  61. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx_cli.egg-info/entry_points.txt +0 -0
  62. {kx_cli-0.0.3 → kx_cli-0.0.5}/src/kx_cli.egg-info/top_level.txt +0 -0
  63. {kx_cli-0.0.3 → kx_cli-0.0.5}/tests/test_cli_get.py +0 -0
  64. {kx_cli-0.0.3 → kx_cli-0.0.5}/tests/test_describe.py +0 -0
  65. {kx_cli-0.0.3 → kx_cli-0.0.5}/tests/test_edit.py +0 -0
  66. {kx_cli-0.0.3 → kx_cli-0.0.5}/tests/test_get.py +0 -0
  67. {kx_cli-0.0.3 → kx_cli-0.0.5}/tests/test_index.py +0 -0
  68. {kx_cli-0.0.3 → kx_cli-0.0.5}/tests/test_logs.py +0 -0
  69. {kx_cli-0.0.3 → kx_cli-0.0.5}/tests/test_port_forward.py +0 -0
kx_cli-0.0.5/PKG-INFO ADDED
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: kx-cli
3
+ Version: 0.0.5
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
+ Dynamic: license-file
20
+
21
+ <div align="center">
22
+ <img src="https://raw.githubusercontent.com/jzills/kx/main/assets/banner.svg" alt="kx — kubectl, indexed." width="800"/>
23
+ </div>
24
+
25
+ <br>
26
+
27
+ <div align="center">
28
+
29
+ # kubectl, indexed
30
+
31
+ </div>
32
+
33
+ <div align="center">
34
+
35
+ [![PyPI version](https://img.shields.io/pypi/v/kx-cli?style=flat-square&color=3fb950&labelColor=21262d)](https://pypi.org/project/kx-cli/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/kx-cli?style=flat-square&color=3fb950&labelColor=21262d)](https://pypi.org/project/kx-cli/)
37
+ [![License](https://img.shields.io/github/license/jzills/kx?style=flat-square&color=3fb950&labelColor=21262d)](LICENSE)
38
+ [![CI](https://img.shields.io/github/actions/workflow/status/jzills/kx/pr.yml?style=flat-square&color=3fb950&labelColor=21262d&label=CI)](https://github.com/jzills/kx/actions/workflows/pr.yml)
39
+
40
+ </div>
41
+
42
+ `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.
43
+
44
+ <div align="center">
45
+ <img src="https://raw.githubusercontent.com/jzills/kx/main/demo/demo.gif" alt="kx demo" width="800"/>
46
+ </div>
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install kx-cli
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ ### List resources
57
+
58
+ ```
59
+ kx get <resource> [--match|-m <substring>] [kubectl flags...]
60
+ ```
61
+
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).
63
+
64
+ ```
65
+ $ kx get pods
66
+ X NAME READY STATUS RESTARTS AGE
67
+ 1 api-7d9f4b8c6-xkp2q 1/1 Running 0 2d
68
+ 2 worker-6c8b5f7d9-mnt4r 1/1 Running 3 5h
69
+ 3 postgres-0 1/1 Running 0 12d
70
+ ```
71
+
72
+ All subsequent commands reference resources by their `X` index from the last `kx get`.
73
+
74
+ ### Commands
75
+
76
+ <!-- commands-table-start -->
77
+ | Command | Description |
78
+ |---|---|
79
+ | `kx get <resource> [--match/-m text] [kubectl flags...]` | List resources and assign index numbers for use with other commands. |
80
+ | `kx describe <indexes>... [kubectl flags...]` | Show full kubectl describe output for one or more indexed resources. |
81
+ | `kx events <indexes>...` | Show Kubernetes events for one or more indexed resources. |
82
+ | `kx logs <index> [kubectl flags...]` | Stream logs for an indexed resource; aggregates across pods for Deployments, StatefulSets, DaemonSets, and Services. |
83
+ | `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 text]` | Print the raw YAML manifest for one or more indexed resources; --show filters to specific top-level fields. |
85
+ | `kx delete <indexes>... [--yes/-y]` | Delete one or more indexed resources (prompts for confirmation unless --yes). |
86
+ | `kx edit <index> [kubectl flags...]` | Open an indexed resource in your editor via kubectl edit. |
87
+ | `kx exec <index> [<cmd>...] [kubectl flags...]` | Open an interactive shell in an indexed pod (bash, falling back to sh). |
88
+ | `kx tree <index> [--index/-i]` | Show the ownership graph for an indexed resource; --index assigns indexes to tree nodes. |
89
+ | `kx rollout <action> <index>` | Run a rollout action (status, restart, pause, resume, history, undo) on a Deployment, StatefulSet, or DaemonSet. |
90
+ | `kx scale <index> <replicas>` | Scale an indexed Deployment, StatefulSet, or ReplicaSet to a given replica count. |
91
+ | `kx port-forward <index> <port> [kubectl flags...]` | Forward a local port to an indexed resource (Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Service). |
92
+ | `kx namespace <index>` | Switch to an indexed namespace; alias: kx ns (run kx get namespaces first). |
93
+ | `kx state [<position>] [--all/-a]` | Show current state, jump to a history position, or list all entries with --all. |
94
+ | `kx drop <position>` | Remove a history entry by position (shown in kx state --all). |
95
+ | `kx back` | Navigate to the previous kx get result. |
96
+ | `kx forward` | Navigate to the next kx get result. |
97
+ <!-- commands-table-end -->
98
+
99
+ ### Example workflow
100
+
101
+ ```bash
102
+ # list deployments, pick index 2
103
+ kx get deployments
104
+ kx describe 2
105
+
106
+ # check events on that deployment
107
+ kx events 2
108
+
109
+ # drill into a pod
110
+ kx get pods
111
+ kx logs 1
112
+ kx exec 1 # opens bash/sh
113
+ kx exec 1 -- env # run a specific command
114
+
115
+ # forward local port 8080 to port 80 on a service
116
+ kx get services
117
+ kx port-forward 2 8080:80
118
+
119
+ # navigate history after multiple gets
120
+ kx get pods
121
+ kx get deployments
122
+ kx logs 1 # logs from pod index 1
123
+ kx state --all # review full history
124
+
125
+ # clean up
126
+ kx delete 3
127
+ ```
128
+
129
+ ## State
130
+
131
+ `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
+
133
+ ```
134
+ $ kx get pods # saves a new entry, cursor advances
135
+ $ kx get deployments # saves another entry, cursor advances
136
+ $ kx logs 1 # resolves index 1 from the pods result
137
+ $ kx state --all # lists all history entries and the current position
138
+ ```
139
+
140
+ Use `kx state <position>` to jump directly to any history entry, and `kx drop <position>` to remove one.
141
+
142
+ ## Development
143
+
144
+ ```bash
145
+ python -m venv .venv
146
+ source .venv/bin/activate
147
+ pip install -e ".[dev]"
148
+ ```
149
+
150
+ Run the CLI directly:
151
+
152
+ ```bash
153
+ python -m kx.main --help
154
+ ```
kx_cli-0.0.5/README.md ADDED
@@ -0,0 +1,134 @@
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
+ [![PyPI version](https://img.shields.io/pypi/v/kx-cli?style=flat-square&color=3fb950&labelColor=21262d)](https://pypi.org/project/kx-cli/)
16
+ [![Python](https://img.shields.io/pypi/pyversions/kx-cli?style=flat-square&color=3fb950&labelColor=21262d)](https://pypi.org/project/kx-cli/)
17
+ [![License](https://img.shields.io/github/license/jzills/kx?style=flat-square&color=3fb950&labelColor=21262d)](LICENSE)
18
+ [![CI](https://img.shields.io/github/actions/workflow/status/jzills/kx/pr.yml?style=flat-square&color=3fb950&labelColor=21262d&label=CI)](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
+ X NAME READY STATUS RESTARTS AGE
47
+ 1 api-7d9f4b8c6-xkp2q 1/1 Running 0 2d
48
+ 2 worker-6c8b5f7d9-mnt4r 1/1 Running 3 5h
49
+ 3 postgres-0 1/1 Running 0 12d
50
+ ```
51
+
52
+ All subsequent commands reference resources by their `X` index from the last `kx get`.
53
+
54
+ ### Commands
55
+
56
+ <!-- commands-table-start -->
57
+ | Command | Description |
58
+ |---|---|
59
+ | `kx get <resource> [--match/-m text] [kubectl flags...]` | List resources and assign index numbers for use with other commands. |
60
+ | `kx describe <indexes>... [kubectl flags...]` | Show full kubectl describe output for one or more indexed resources. |
61
+ | `kx events <indexes>...` | Show Kubernetes events for one or more indexed resources. |
62
+ | `kx logs <index> [kubectl flags...]` | Stream logs for an indexed resource; aggregates across pods for Deployments, StatefulSets, DaemonSets, and Services. |
63
+ | `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 text]` | Print the raw YAML manifest for one or more indexed resources; --show filters to specific top-level fields. |
65
+ | `kx delete <indexes>... [--yes/-y]` | Delete one or more indexed resources (prompts for confirmation unless --yes). |
66
+ | `kx edit <index> [kubectl flags...]` | Open an indexed resource in your editor via kubectl edit. |
67
+ | `kx exec <index> [<cmd>...] [kubectl flags...]` | Open an interactive shell in an indexed pod (bash, falling back to sh). |
68
+ | `kx tree <index> [--index/-i]` | Show the ownership graph for an indexed resource; --index assigns indexes to tree nodes. |
69
+ | `kx rollout <action> <index>` | Run a rollout action (status, restart, pause, resume, history, undo) on a Deployment, StatefulSet, or DaemonSet. |
70
+ | `kx scale <index> <replicas>` | Scale an indexed Deployment, StatefulSet, or ReplicaSet to a given replica count. |
71
+ | `kx port-forward <index> <port> [kubectl flags...]` | Forward a local port to an indexed resource (Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Service). |
72
+ | `kx namespace <index>` | Switch to an indexed namespace; alias: kx ns (run kx get namespaces first). |
73
+ | `kx state [<position>] [--all/-a]` | Show current state, jump to a history position, or list all entries with --all. |
74
+ | `kx drop <position>` | Remove a history entry by position (shown in kx state --all). |
75
+ | `kx back` | Navigate to the previous kx get result. |
76
+ | `kx forward` | Navigate to the next kx get result. |
77
+ <!-- commands-table-end -->
78
+
79
+ ### Example workflow
80
+
81
+ ```bash
82
+ # list deployments, pick index 2
83
+ kx get deployments
84
+ kx describe 2
85
+
86
+ # check events on that deployment
87
+ kx events 2
88
+
89
+ # drill into a pod
90
+ kx get pods
91
+ kx logs 1
92
+ kx exec 1 # opens bash/sh
93
+ kx exec 1 -- env # run a specific command
94
+
95
+ # forward local port 8080 to port 80 on a service
96
+ kx get services
97
+ kx port-forward 2 8080:80
98
+
99
+ # navigate history after multiple gets
100
+ kx get pods
101
+ kx get deployments
102
+ kx logs 1 # logs from pod index 1
103
+ kx state --all # review full history
104
+
105
+ # clean up
106
+ kx delete 3
107
+ ```
108
+
109
+ ## State
110
+
111
+ `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
+
113
+ ```
114
+ $ kx get pods # saves a new entry, cursor advances
115
+ $ kx get deployments # saves another entry, cursor advances
116
+ $ kx logs 1 # resolves index 1 from the pods result
117
+ $ kx state --all # lists all history entries and the current position
118
+ ```
119
+
120
+ Use `kx state <position>` to jump directly to any history entry, and `kx drop <position>` to remove one.
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ python -m venv .venv
126
+ source .venv/bin/activate
127
+ pip install -e ".[dev]"
128
+ ```
129
+
130
+ Run the CLI directly:
131
+
132
+ ```bash
133
+ python -m kx.main --help
134
+ ```
@@ -1,12 +1,17 @@
1
1
  [project]
2
2
  name = "kx-cli"
3
- version = "0.0.3"
3
+ version = "0.0.5"
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==0.25.1",
9
- "click",
14
+ "typer==0.26.8",
10
15
  "kubernetes",
11
16
  "rich",
12
17
  ]
@@ -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)
@@ -0,0 +1,9 @@
1
+ from kx.state import StateHistory, StateServiceProtocol
2
+
3
+
4
+ class DropCommand:
5
+ def __init__(self, state: StateServiceProtocol):
6
+ self.state = state
7
+
8
+ def execute(self, position: int) -> StateHistory:
9
+ return self.state.drop(position)
@@ -0,0 +1,54 @@
1
+ import subprocess
2
+
3
+ from kx.kinds import Kind
4
+ from kx.kubectl import KubectlServiceProtocol
5
+ from kx.state import StateServiceProtocol
6
+
7
+
8
+ class ExecCommand:
9
+ def __init__(
10
+ self,
11
+ state: StateServiceProtocol,
12
+ kubectl: KubectlServiceProtocol,
13
+ shells: tuple[str, ...] = ("bash", "sh"),
14
+ ):
15
+ self.state = state
16
+ self.kubectl = kubectl
17
+ self.shells = shells
18
+
19
+ def execute(
20
+ self, index: int, cmd: list[str] | None, extra_args: list[str] = []
21
+ ) -> None:
22
+ name, namespace, kind = self.state.fields(index)
23
+ if kind != Kind.Pod:
24
+ raise ValueError("exec is only supported for pods.")
25
+ if cmd:
26
+ rc = self.kubectl.run_interactive(
27
+ ["exec", "-it", name, "-n", namespace, *extra_args, "--", *cmd],
28
+ stderr=subprocess.DEVNULL,
29
+ )
30
+ if rc != 0:
31
+ raise ValueError(f"Command failed in container (exit {rc}).")
32
+ else:
33
+ for shell in self.shells:
34
+ probe_rc = self.kubectl.probe(
35
+ [
36
+ "exec",
37
+ name,
38
+ "-n",
39
+ namespace,
40
+ *extra_args,
41
+ "--",
42
+ shell,
43
+ "-c",
44
+ "exit 0",
45
+ ]
46
+ )
47
+ if probe_rc == 0:
48
+ self.kubectl.run_interactive(
49
+ ["exec", "-it", name, "-n", namespace, *extra_args, "--", shell]
50
+ )
51
+ return
52
+ raise ValueError(
53
+ "No shell found in container. Provide an explicit command: kx exec <index> -- /path/to/binary"
54
+ )
@@ -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)
@@ -0,0 +1,16 @@
1
+ import json
2
+
3
+ from kx.kubectl import KubectlServiceProtocol
4
+ from kx.state import StateServiceProtocol
5
+
6
+
7
+ class LabelsCommand:
8
+ def __init__(self, state: StateServiceProtocol, kubectl: KubectlServiceProtocol):
9
+ self.state = state
10
+ self.kubectl = kubectl
11
+
12
+ def execute(self, index: int) -> dict[str, str]:
13
+ name, namespace, kind = self.state.fields(index)
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 {}
@@ -2,11 +2,12 @@ from kx.kubectl import KubectlServiceProtocol
2
2
  from kx.state import StateServiceProtocol
3
3
 
4
4
 
5
- class YamlCommand:
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, namespace, kind = self.state.fields(index)
12
- return self.kubectl.run(["get", kind, name, "-n", namespace, "-o", "yaml"])
11
+ name, _, _ = self.state.fields(index)
12
+ self.kubectl.run(["config", "set-context", "--current", f"--namespace={name}"])
13
+ return name
@@ -0,0 +1,35 @@
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
+ _INTERACTIVE_ACTIONS = {"status"}
9
+
10
+
11
+ class RolloutAction(str, Enum):
12
+ status = "status"
13
+ restart = "restart"
14
+ pause = "pause"
15
+ resume = "resume"
16
+ history = "history"
17
+ undo = "undo"
18
+
19
+
20
+ class RolloutCommand:
21
+ def __init__(self, kubectl: KubectlServiceProtocol, state: StateServiceProtocol):
22
+ self.kubectl = kubectl
23
+ self.state = state
24
+
25
+ def execute(
26
+ self, index: int, action: RolloutAction = RolloutAction.status
27
+ ) -> str | None:
28
+ name, namespace, kind = self.state.fields(index)
29
+ if kind not in _SUPPORTED_KINDS:
30
+ raise ValueError(f"rollout is not supported for '{kind}'.")
31
+ args = ["rollout", action.value, f"{kind}/{name}", "-n", namespace]
32
+ if action.value in _INTERACTIVE_ACTIONS:
33
+ self.kubectl.run_interactive(args)
34
+ return None
35
+ return self.kubectl.run(args)
@@ -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,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)
@@ -0,0 +1,38 @@
1
+ import os
2
+ import tomllib
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ _CONFIG_FILE = Path.home() / ".kx" / "config.toml"
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class Config:
11
+ max_history: int = 10
12
+ shells: tuple[str, ...] = ("bash", "sh")
13
+
14
+
15
+ def load_config() -> Config:
16
+ kwargs: dict = {}
17
+
18
+ if _CONFIG_FILE.exists():
19
+ try:
20
+ with open(_CONFIG_FILE, "rb") as f:
21
+ data = tomllib.load(f)
22
+ except tomllib.TOMLDecodeError as e:
23
+ raise SystemExit(f"kx: error reading {_CONFIG_FILE}: {e}")
24
+ if "max_history" in data:
25
+ kwargs["max_history"] = data["max_history"]
26
+ if "shells" in data:
27
+ kwargs["shells"] = tuple(data["shells"])
28
+
29
+ if "KX_MAX_HISTORY" in os.environ:
30
+ try:
31
+ kwargs["max_history"] = int(os.environ["KX_MAX_HISTORY"])
32
+ except ValueError:
33
+ raise SystemExit("kx: KX_MAX_HISTORY must be an integer")
34
+
35
+ if "KX_SHELLS" in os.environ:
36
+ kwargs["shells"] = tuple(os.environ["KX_SHELLS"].split(","))
37
+
38
+ return Config(**kwargs)