kx-cli 0.0.10__tar.gz → 0.0.11__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.10/src/kx_cli.egg-info → kx_cli-0.0.11}/PKG-INFO +20 -4
- {kx_cli-0.0.10 → kx_cli-0.0.11}/README.md +19 -3
- {kx_cli-0.0.10 → kx_cli-0.0.11}/pyproject.toml +1 -1
- kx_cli-0.0.11/src/kx/commands/secret.py +58 -0
- kx_cli-0.0.11/src/kx/commands/tree.py +65 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/console.py +22 -2
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/graph.py +120 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/main.py +219 -15
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/types.py +2 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11/src/kx_cli.egg-info}/PKG-INFO +20 -4
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx_cli.egg-info/SOURCES.txt +3 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_cli_help.py +22 -0
- kx_cli-0.0.11/tests/test_cli_secret.py +352 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_graph.py +120 -1
- kx_cli-0.0.11/tests/test_secret.py +100 -0
- kx_cli-0.0.11/tests/test_tree.py +130 -0
- kx_cli-0.0.10/src/kx/commands/tree.py +0 -34
- kx_cli-0.0.10/tests/test_tree.py +0 -59
- {kx_cli-0.0.10 → kx_cli-0.0.11}/LICENSE +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/setup.cfg +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/__init__.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/__init__.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/_metadata.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/annotations.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/back.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/context.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/contexts.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/delete.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/describe.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/diagnostic.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/drop.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/edit.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/events.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/exec.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/forward.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/get.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/labels.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/logs.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/metadata_write.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/namespace.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/port_forward.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/rollout.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/scale.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/scan.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/state.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/theme.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/top.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/yaml.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/config.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/diagnostics.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/errors.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/events.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/index.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/k8s.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/kinds.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/kubectl.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/refresh.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/scanner.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/state.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/themes.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx_cli.egg-info/dependency_links.txt +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx_cli.egg-info/entry_points.txt +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx_cli.egg-info/requires.txt +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx_cli.egg-info/top_level.txt +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_annotations.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_back.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_cli_context.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_cli_get.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_cli_kind_alias.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_cli_label_annotate.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_cli_top.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_config.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_console.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_context.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_contexts.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_delete.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_describe.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_diagnostic.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_diagnostics.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_drop.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_edit.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_error_handling.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_events.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_exec.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_forward.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_get.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_index.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_kinds.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_kubectl.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_labels.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_logs.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_metadata_helper.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_metadata_write.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_namespace.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_port_forward.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_refresh.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_rollout.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_scale.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_scan.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_scanner.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_state.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_theme.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_themes.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_top.py +0 -0
- {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_yaml.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kx-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.11
|
|
4
4
|
Summary: kubectl wrapper with index-based resource selection
|
|
5
5
|
Classifier: Programming Language :: Python :: 3
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -92,14 +92,15 @@ pipx run --spec kx-cli kx get pods
|
|
|
92
92
|
|
|
93
93
|
Known kinds can drop the `get`: `kx pods`, `kx deploy -n kube-system`, `kx svc --match api` — kubectl shorthands (`po`, `deploy`, `svc`, `sts`, ...) included. An integer after a kind relists just that index: `kx po 3`. CRDs and other resource types still use `kx get <resource>`.
|
|
94
94
|
|
|
95
|
-
Global flags: `--no-color` disables styled output, `-v`/`--version` prints the installed version, and
|
|
95
|
+
Global flags: `--no-color` disables styled output, `-v`/`--version` prints the installed version, and `-h`/`--help` on any command shows usage, examples, and aliases.
|
|
96
96
|
|
|
97
97
|
### Commands
|
|
98
98
|
|
|
99
99
|
<!-- commands-table-start -->
|
|
100
100
|
| Command | Description |
|
|
101
101
|
|---|---|
|
|
102
|
-
| `kx get <resource> [--match/-m str] [kubectl flags...]` | List resources and assign index numbers for use with other commands; shorthand: kx <kind> (e.g. kx pods, kx po 3). |
|
|
102
|
+
| `kx get <resource> [--match/-m str] [--decode] [--key/-k str] [--yes/-y] [kubectl flags...]` | List resources and assign index numbers for use with other commands; shorthand: kx <kind> (e.g. kx pods, kx po 3). |
|
|
103
|
+
| `kx secret [--match/-m str] [--decode] [--key/-k str] [--yes/-y] [kubectl flags...]` | List Secrets like kx get, or show an indexed Secret's data with --decode; alias: kx secrets. |
|
|
103
104
|
| `kx top [--match/-m str] [--no-limits] [kubectl flags...]` | List CPU/memory usage for pods in the current namespace and assign index numbers, like kx get; shows usage as a percent of each pod's resource limits unless --no-limits. |
|
|
104
105
|
| `kx describe <indexes>... [kubectl flags...]` | Show full kubectl describe output for one or more indexed resources. |
|
|
105
106
|
| `kx events <indexes>...` | Show Kubernetes events for one or more indexed resources. |
|
|
@@ -113,7 +114,7 @@ Global flags: `--no-color` disables styled output, `-v`/`--version` prints the i
|
|
|
113
114
|
| `kx delete <indexes>... [--yes/-y]` | Delete one or more indexed resources (prompts for confirmation unless --yes). |
|
|
114
115
|
| `kx edit <index> [kubectl flags...]` | Open an indexed resource in your editor via kubectl edit. |
|
|
115
116
|
| `kx exec <index> [<cmd>...] [kubectl flags...]` | Open an interactive shell in an indexed pod (bash, falling back to sh). |
|
|
116
|
-
| `kx tree <index> [--index/-i]` | Show the ownership graph for an indexed resource; --index assigns indexes to tree nodes. |
|
|
117
|
+
| `kx tree [<index>] [--index/-i]` | Show the ownership graph for an indexed resource, or the whole current namespace when no index is given; --index assigns indexes to tree nodes. A Namespace index graphs that namespace. |
|
|
117
118
|
| `kx rollout <action> <index>` | Run a rollout action (status, restart, pause, resume, history, undo) on a Deployment, StatefulSet, or DaemonSet. |
|
|
118
119
|
| `kx scale <index> <replicas>` | Scale an indexed Deployment, StatefulSet, or ReplicaSet to a given replica count. |
|
|
119
120
|
| `kx port-forward <index> <port> [kubectl flags...]` | Forward a local port to an indexed resource (Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Service). |
|
|
@@ -147,6 +148,21 @@ CronJob runs, usage near limits), a per-pod status table, recent log tails
|
|
|
147
148
|
from broken containers, and warning events — one screen instead of four
|
|
148
149
|
kubectl commands.
|
|
149
150
|
|
|
151
|
+
### Read a Secret in plaintext
|
|
152
|
+
|
|
153
|
+
`kx secret <index> --decode` prints an indexed Secret's keys and values
|
|
154
|
+
decoded, instead of the base64 kubectl returns. Values that aren't text show a
|
|
155
|
+
`<binary, N bytes>` placeholder rather than garbling the table. `--key`/`-k`
|
|
156
|
+
prints a single value raw — no banner, no wrapping — so it drops straight into
|
|
157
|
+
a shell: `export PGPASSWORD=$(kx secret 1 --decode -k password)`, or redirect a
|
|
158
|
+
binary value to a file. Bare `kx secret --decode` decodes every Secret in the
|
|
159
|
+
namespace in one call, `-n` included — it confirms first unless you pass
|
|
160
|
+
`--yes`/`-y`, since that prints every credential in the namespace.
|
|
161
|
+
|
|
162
|
+
<div align="center">
|
|
163
|
+
<img src="https://raw.githubusercontent.com/jzills/kx/main/demo/secret.gif" alt="kx secret --decode demo" width="800"/>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
150
166
|
### Scan images for vulnerabilities
|
|
151
167
|
|
|
152
168
|
`kx scan <index>` scans the unique container images of an indexed workload
|
|
@@ -69,14 +69,15 @@ pipx run --spec kx-cli kx get pods
|
|
|
69
69
|
|
|
70
70
|
Known kinds can drop the `get`: `kx pods`, `kx deploy -n kube-system`, `kx svc --match api` — kubectl shorthands (`po`, `deploy`, `svc`, `sts`, ...) included. An integer after a kind relists just that index: `kx po 3`. CRDs and other resource types still use `kx get <resource>`.
|
|
71
71
|
|
|
72
|
-
Global flags: `--no-color` disables styled output, `-v`/`--version` prints the installed version, and
|
|
72
|
+
Global flags: `--no-color` disables styled output, `-v`/`--version` prints the installed version, and `-h`/`--help` on any command shows usage, examples, and aliases.
|
|
73
73
|
|
|
74
74
|
### Commands
|
|
75
75
|
|
|
76
76
|
<!-- commands-table-start -->
|
|
77
77
|
| Command | Description |
|
|
78
78
|
|---|---|
|
|
79
|
-
| `kx get <resource> [--match/-m str] [kubectl flags...]` | List resources and assign index numbers for use with other commands; shorthand: kx <kind> (e.g. kx pods, kx po 3). |
|
|
79
|
+
| `kx get <resource> [--match/-m str] [--decode] [--key/-k str] [--yes/-y] [kubectl flags...]` | List resources and assign index numbers for use with other commands; shorthand: kx <kind> (e.g. kx pods, kx po 3). |
|
|
80
|
+
| `kx secret [--match/-m str] [--decode] [--key/-k str] [--yes/-y] [kubectl flags...]` | List Secrets like kx get, or show an indexed Secret's data with --decode; alias: kx secrets. |
|
|
80
81
|
| `kx top [--match/-m str] [--no-limits] [kubectl flags...]` | List CPU/memory usage for pods in the current namespace and assign index numbers, like kx get; shows usage as a percent of each pod's resource limits unless --no-limits. |
|
|
81
82
|
| `kx describe <indexes>... [kubectl flags...]` | Show full kubectl describe output for one or more indexed resources. |
|
|
82
83
|
| `kx events <indexes>...` | Show Kubernetes events for one or more indexed resources. |
|
|
@@ -90,7 +91,7 @@ Global flags: `--no-color` disables styled output, `-v`/`--version` prints the i
|
|
|
90
91
|
| `kx delete <indexes>... [--yes/-y]` | Delete one or more indexed resources (prompts for confirmation unless --yes). |
|
|
91
92
|
| `kx edit <index> [kubectl flags...]` | Open an indexed resource in your editor via kubectl edit. |
|
|
92
93
|
| `kx exec <index> [<cmd>...] [kubectl flags...]` | Open an interactive shell in an indexed pod (bash, falling back to sh). |
|
|
93
|
-
| `kx tree <index> [--index/-i]` | Show the ownership graph for an indexed resource; --index assigns indexes to tree nodes. |
|
|
94
|
+
| `kx tree [<index>] [--index/-i]` | Show the ownership graph for an indexed resource, or the whole current namespace when no index is given; --index assigns indexes to tree nodes. A Namespace index graphs that namespace. |
|
|
94
95
|
| `kx rollout <action> <index>` | Run a rollout action (status, restart, pause, resume, history, undo) on a Deployment, StatefulSet, or DaemonSet. |
|
|
95
96
|
| `kx scale <index> <replicas>` | Scale an indexed Deployment, StatefulSet, or ReplicaSet to a given replica count. |
|
|
96
97
|
| `kx port-forward <index> <port> [kubectl flags...]` | Forward a local port to an indexed resource (Pod, Deployment, ReplicaSet, StatefulSet, DaemonSet, Service). |
|
|
@@ -124,6 +125,21 @@ CronJob runs, usage near limits), a per-pod status table, recent log tails
|
|
|
124
125
|
from broken containers, and warning events — one screen instead of four
|
|
125
126
|
kubectl commands.
|
|
126
127
|
|
|
128
|
+
### Read a Secret in plaintext
|
|
129
|
+
|
|
130
|
+
`kx secret <index> --decode` prints an indexed Secret's keys and values
|
|
131
|
+
decoded, instead of the base64 kubectl returns. Values that aren't text show a
|
|
132
|
+
`<binary, N bytes>` placeholder rather than garbling the table. `--key`/`-k`
|
|
133
|
+
prints a single value raw — no banner, no wrapping — so it drops straight into
|
|
134
|
+
a shell: `export PGPASSWORD=$(kx secret 1 --decode -k password)`, or redirect a
|
|
135
|
+
binary value to a file. Bare `kx secret --decode` decodes every Secret in the
|
|
136
|
+
namespace in one call, `-n` included — it confirms first unless you pass
|
|
137
|
+
`--yes`/`-y`, since that prints every credential in the namespace.
|
|
138
|
+
|
|
139
|
+
<div align="center">
|
|
140
|
+
<img src="https://raw.githubusercontent.com/jzills/kx/main/demo/secret.gif" alt="kx secret --decode demo" width="800"/>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
127
143
|
### Scan images for vulnerabilities
|
|
128
144
|
|
|
129
145
|
`kx scan <index>` scans the unique container images of an indexed workload
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
5
|
+
from kx.state import StateServiceProtocol
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def to_display(value: bytes) -> str:
|
|
9
|
+
"""Decoded text, or a placeholder for values that aren't valid UTF-8.
|
|
10
|
+
|
|
11
|
+
Keeps keystores and other binary payloads from garbling the table; `--key`
|
|
12
|
+
still emits their raw bytes so they can be redirected to a file."""
|
|
13
|
+
try:
|
|
14
|
+
return value.decode("utf-8")
|
|
15
|
+
except UnicodeDecodeError:
|
|
16
|
+
return f"<binary, {len(value)} bytes>"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _decode_data(obj: dict) -> dict[str, bytes]:
|
|
20
|
+
"""Decoded `data` for one Secret object. Values stay `bytes` so the render
|
|
21
|
+
layer owns the text/binary decision. `stringData` is write-only and never
|
|
22
|
+
returned by the API, so `data` holds everything."""
|
|
23
|
+
return {
|
|
24
|
+
key: base64.b64decode(value) for key, value in (obj.get("data") or {}).items()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SecretCommand:
|
|
29
|
+
def __init__(self, state: StateServiceProtocol, kubectl: KubectlServiceProtocol):
|
|
30
|
+
self.state = state
|
|
31
|
+
self.kubectl = kubectl
|
|
32
|
+
|
|
33
|
+
def execute(self, index: int) -> dict[str, bytes]:
|
|
34
|
+
"""Decoded data for the indexed Secret."""
|
|
35
|
+
name, namespace, kind = self.state.fields(index)
|
|
36
|
+
raw = self.kubectl.run(
|
|
37
|
+
["get", kind, name, "-n", namespace, "-o", "json"],
|
|
38
|
+
)
|
|
39
|
+
return _decode_data(json.loads(raw))
|
|
40
|
+
|
|
41
|
+
def execute_all(
|
|
42
|
+
self, extra_args: list[str] | None = None
|
|
43
|
+
) -> list[tuple[str, str, dict[str, bytes]]]:
|
|
44
|
+
"""Every Secret in the target namespace as (name, namespace, data).
|
|
45
|
+
|
|
46
|
+
One kubectl call returns the whole list with data attached, so a
|
|
47
|
+
namespace sweep costs the same as a single fetch. `extra_args` carries
|
|
48
|
+
the user's kubectl flags so `-n` selects the namespace."""
|
|
49
|
+
raw = self.kubectl.run(["get", "secret", "-o", "json", *(extra_args or [])])
|
|
50
|
+
obj = json.loads(raw)
|
|
51
|
+
return [
|
|
52
|
+
(
|
|
53
|
+
item.get("metadata", {}).get("name", ""),
|
|
54
|
+
item.get("metadata", {}).get("namespace", ""),
|
|
55
|
+
_decode_data(item),
|
|
56
|
+
)
|
|
57
|
+
for item in obj.get("items") or []
|
|
58
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from rich.tree import Tree
|
|
2
|
+
|
|
3
|
+
from kx.kinds import Kind
|
|
4
|
+
from kx.kubectl import KubectlServiceProtocol
|
|
5
|
+
from kx.state import State, StateServiceProtocol
|
|
6
|
+
from kx.types import (
|
|
7
|
+
BuildIndexedTree,
|
|
8
|
+
BuildNamespaceIndexedTree,
|
|
9
|
+
BuildNamespaceTree,
|
|
10
|
+
BuildTree,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TreeCommand:
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
state: StateServiceProtocol,
|
|
18
|
+
kubectl: KubectlServiceProtocol,
|
|
19
|
+
build_tree: BuildTree,
|
|
20
|
+
build_indexed_tree: BuildIndexedTree,
|
|
21
|
+
build_namespace_tree: BuildNamespaceTree,
|
|
22
|
+
build_namespace_indexed_tree: BuildNamespaceIndexedTree,
|
|
23
|
+
):
|
|
24
|
+
self.state = state
|
|
25
|
+
self.kubectl = kubectl
|
|
26
|
+
self.build_tree = build_tree
|
|
27
|
+
self.build_indexed_tree = build_indexed_tree
|
|
28
|
+
self.build_namespace_tree = build_namespace_tree
|
|
29
|
+
self.build_namespace_indexed_tree = build_namespace_indexed_tree
|
|
30
|
+
|
|
31
|
+
def execute(self, index: int, indexed: bool = False) -> Tree:
|
|
32
|
+
name, namespace, kind = self.state.fields(index)
|
|
33
|
+
# A Namespace row walks that namespace itself — its own name, not the
|
|
34
|
+
# namespace the `kx get ns` was run in.
|
|
35
|
+
if kind == Kind.Namespace:
|
|
36
|
+
return self._namespace(name, indexed)
|
|
37
|
+
if indexed:
|
|
38
|
+
tree, resources = self.build_indexed_tree(kind, name, namespace)
|
|
39
|
+
if resources:
|
|
40
|
+
self.state.save(
|
|
41
|
+
State(
|
|
42
|
+
resources={name: kind for name, kind in resources},
|
|
43
|
+
namespace=namespace,
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
tree = self.build_tree(kind, name, namespace)
|
|
48
|
+
return tree
|
|
49
|
+
|
|
50
|
+
def execute_namespace(self, namespace: str, indexed: bool = False) -> Tree:
|
|
51
|
+
return self._namespace(namespace, indexed)
|
|
52
|
+
|
|
53
|
+
def _namespace(self, namespace: str, indexed: bool) -> Tree:
|
|
54
|
+
if indexed:
|
|
55
|
+
tree, resources = self.build_namespace_indexed_tree(namespace)
|
|
56
|
+
if resources:
|
|
57
|
+
self.state.save(
|
|
58
|
+
State(
|
|
59
|
+
resources={name: kind for name, kind in resources},
|
|
60
|
+
namespace=namespace,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
tree = self.build_namespace_tree(namespace)
|
|
65
|
+
return tree
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import json
|
|
3
|
+
import sys
|
|
3
4
|
import threading
|
|
4
5
|
from contextlib import contextmanager
|
|
5
6
|
from datetime import datetime, timezone
|
|
@@ -194,6 +195,25 @@ def print_raw(text: str) -> None:
|
|
|
194
195
|
_console.print(text, markup=False, highlight=False)
|
|
195
196
|
|
|
196
197
|
|
|
198
|
+
def write_value(value: bytes) -> None:
|
|
199
|
+
"""Write a secret value to stdout unstyled, bypassing Rich entirely.
|
|
200
|
+
|
|
201
|
+
Rich wraps at the console width (_PIPE_WIDTH off-terminal), which would
|
|
202
|
+
inject newlines into a long value — a cert or token — and corrupt
|
|
203
|
+
`$(kx secret 1 --decode -k tls.crt)`. Writing to the byte buffer also keeps
|
|
204
|
+
`> store.p12` byte-exact. The trailing newline is added only for text, so
|
|
205
|
+
binary redirects reproduce the file exactly while text stays shell-friendly.
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
value.decode("utf-8")
|
|
209
|
+
except UnicodeDecodeError:
|
|
210
|
+
payload = value
|
|
211
|
+
else:
|
|
212
|
+
payload = value + b"\n"
|
|
213
|
+
sys.stdout.buffer.write(payload)
|
|
214
|
+
sys.stdout.buffer.flush()
|
|
215
|
+
|
|
216
|
+
|
|
197
217
|
def print_rich(renderable) -> None:
|
|
198
218
|
_console.print(renderable)
|
|
199
219
|
|
|
@@ -667,7 +687,7 @@ def print_command_help(ctx) -> None:
|
|
|
667
687
|
names = " ".join(opt.opts)
|
|
668
688
|
_console.print(f" [body]{names:<20}[/body] [muted]{opt.help or ''}[/muted]")
|
|
669
689
|
_console.print(
|
|
670
|
-
f" [body]{'--help':<20}[/body] [muted]Show this message and exit.[/muted]"
|
|
690
|
+
f" [body]{'-h, --help':<20}[/body] [muted]Show this message and exit.[/muted]"
|
|
671
691
|
)
|
|
672
692
|
|
|
673
693
|
aliases = getattr(ctx.command.callback, "_aliases", [])
|
|
@@ -725,7 +745,7 @@ def print_help(
|
|
|
725
745
|
f" [body]{'-v, --version':<14}[/body] [muted]Show the kx version and exit.[/muted]"
|
|
726
746
|
)
|
|
727
747
|
_console.print(
|
|
728
|
-
f" [body]{'--help':<14}[/body] [muted]Show this message and exit.[/muted]"
|
|
748
|
+
f" [body]{'-h, --help':<14}[/body] [muted]Show this message and exit.[/muted]"
|
|
729
749
|
)
|
|
730
750
|
if version:
|
|
731
751
|
_console.print()
|
|
@@ -16,6 +16,17 @@ def build_indexed_tree(
|
|
|
16
16
|
return _build(kind, name, namespace, indexed=True)
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def build_namespace_tree(namespace: str) -> Tree:
|
|
20
|
+
root, _ = _build_namespace(namespace, indexed=False)
|
|
21
|
+
return root
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_namespace_indexed_tree(
|
|
25
|
+
namespace: str,
|
|
26
|
+
) -> tuple[Tree, list[tuple[str, str]]]:
|
|
27
|
+
return _build_namespace(namespace, indexed=True)
|
|
28
|
+
|
|
29
|
+
|
|
19
30
|
def _build(
|
|
20
31
|
kind: str, name: str, namespace: str, indexed: bool
|
|
21
32
|
) -> tuple[Tree, list[tuple[str, str]]]:
|
|
@@ -54,6 +65,115 @@ def _build(
|
|
|
54
65
|
return root, resources
|
|
55
66
|
|
|
56
67
|
|
|
68
|
+
# Prefix + semantic color for each node kind in a namespace forest. Controllers
|
|
69
|
+
# render as roots with their full kind name; owned resources use the short
|
|
70
|
+
# prefixes the single-resource tree already uses (rs/job/pod).
|
|
71
|
+
_NODE_STYLE: dict[Kind, tuple[str, str]] = {
|
|
72
|
+
Kind.Deployment: ("Deployment", "header"),
|
|
73
|
+
Kind.StatefulSet: ("StatefulSet", "header"),
|
|
74
|
+
Kind.DaemonSet: ("DaemonSet", "header"),
|
|
75
|
+
Kind.CronJob: ("CronJob", "header"),
|
|
76
|
+
Kind.Job: ("job", "accent"),
|
|
77
|
+
Kind.ReplicaSet: ("rs", "accent"),
|
|
78
|
+
Kind.Pod: ("pod", "body"),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Stable order roots appear under the Namespace node.
|
|
82
|
+
_ROOT_ORDER: list[Kind] = [
|
|
83
|
+
Kind.Deployment,
|
|
84
|
+
Kind.StatefulSet,
|
|
85
|
+
Kind.DaemonSet,
|
|
86
|
+
Kind.CronJob,
|
|
87
|
+
Kind.Job,
|
|
88
|
+
Kind.ReplicaSet,
|
|
89
|
+
Kind.Pod,
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _build_namespace(
|
|
94
|
+
namespace: str, indexed: bool
|
|
95
|
+
) -> tuple[Tree, list[tuple[str, str]]]:
|
|
96
|
+
"""Full ownership forest for a namespace: every workload controller as a
|
|
97
|
+
root with its owned resources beneath, plus orphan Jobs/ReplicaSets and bare
|
|
98
|
+
Pods so nothing is hidden. Each pod appears exactly once. Unlike `_build`,
|
|
99
|
+
the Namespace root node is not indexed — children number from 1."""
|
|
100
|
+
load_config()
|
|
101
|
+
apps = client.AppsV1Api()
|
|
102
|
+
core = client.CoreV1Api()
|
|
103
|
+
batch = client.BatchV1Api()
|
|
104
|
+
|
|
105
|
+
deployments = apps.list_namespaced_deployment(namespace).items
|
|
106
|
+
replica_sets = apps.list_namespaced_replica_set(namespace).items
|
|
107
|
+
stateful_sets = apps.list_namespaced_stateful_set(namespace).items
|
|
108
|
+
daemon_sets = apps.list_namespaced_daemon_set(namespace).items
|
|
109
|
+
cron_jobs = batch.list_namespaced_cron_job(namespace).items
|
|
110
|
+
jobs = batch.list_namespaced_job(namespace).items
|
|
111
|
+
pods = core.list_namespaced_pod(namespace).items
|
|
112
|
+
|
|
113
|
+
# Resources that can be owned by something else in the namespace, indexed by
|
|
114
|
+
# each owner uid so a parent finds its children in one pass.
|
|
115
|
+
child_kinds = (
|
|
116
|
+
[(rs, Kind.ReplicaSet) for rs in replica_sets]
|
|
117
|
+
+ [(job, Kind.Job) for job in jobs]
|
|
118
|
+
+ [(pod, Kind.Pod) for pod in pods]
|
|
119
|
+
)
|
|
120
|
+
children_by_owner: dict[str, list[tuple[object, Kind]]] = {}
|
|
121
|
+
for obj, kind in child_kinds:
|
|
122
|
+
for ref in obj.metadata.owner_references or []:
|
|
123
|
+
children_by_owner.setdefault(ref.uid, []).append((obj, kind))
|
|
124
|
+
|
|
125
|
+
present_uids = {
|
|
126
|
+
obj.metadata.uid
|
|
127
|
+
for obj in (
|
|
128
|
+
*deployments,
|
|
129
|
+
*replica_sets,
|
|
130
|
+
*stateful_sets,
|
|
131
|
+
*daemon_sets,
|
|
132
|
+
*cron_jobs,
|
|
133
|
+
*jobs,
|
|
134
|
+
*pods,
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Controllers are always roots; an owned child (rs/job/pod) is a root only
|
|
139
|
+
# when orphaned — its owner isn't among the namespace's collected objects.
|
|
140
|
+
roots: list[tuple[object, Kind]] = [
|
|
141
|
+
*[(d, Kind.Deployment) for d in deployments],
|
|
142
|
+
*[(s, Kind.StatefulSet) for s in stateful_sets],
|
|
143
|
+
*[(d, Kind.DaemonSet) for d in daemon_sets],
|
|
144
|
+
*[(c, Kind.CronJob) for c in cron_jobs],
|
|
145
|
+
]
|
|
146
|
+
for obj, kind in child_kinds:
|
|
147
|
+
refs = obj.metadata.owner_references or []
|
|
148
|
+
if not any(ref.uid in present_uids for ref in refs):
|
|
149
|
+
roots.append((obj, kind))
|
|
150
|
+
|
|
151
|
+
order = {kind: position for position, kind in enumerate(_ROOT_ORDER)}
|
|
152
|
+
roots.sort(key=lambda pair: (order[pair[1]], pair[0].metadata.name))
|
|
153
|
+
|
|
154
|
+
resources: list[tuple[str, str]] = []
|
|
155
|
+
root = Tree(f"[header]Namespace/{namespace}[/header]")
|
|
156
|
+
if not roots:
|
|
157
|
+
root.add("[muted](no workloads)[/muted]")
|
|
158
|
+
return root, resources
|
|
159
|
+
|
|
160
|
+
for obj, kind in roots:
|
|
161
|
+
_render(obj, kind, root, children_by_owner, resources, indexed)
|
|
162
|
+
return root, resources
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _render(obj, kind, parent, children_by_owner, resources, indexed):
|
|
166
|
+
"""Add `obj` under `parent`, then recurse into its owned children; a Pod is
|
|
167
|
+
a leaf whose containers are listed directly."""
|
|
168
|
+
prefix, color = _NODE_STYLE[kind]
|
|
169
|
+
node = _add_node(parent, resources, indexed, color, prefix, obj.metadata.name, kind)
|
|
170
|
+
if kind == Kind.Pod:
|
|
171
|
+
_add_containers(obj, node)
|
|
172
|
+
return
|
|
173
|
+
for child, child_kind in children_by_owner.get(obj.metadata.uid, []):
|
|
174
|
+
_render(child, child_kind, node, children_by_owner, resources, indexed)
|
|
175
|
+
|
|
176
|
+
|
|
57
177
|
def _add_node(parent, resources, indexed, color, prefix, name, kind):
|
|
58
178
|
"""Add a labelled child node; when indexed, prepend its 1-based index and
|
|
59
179
|
record `(name, kind)` in `resources`."""
|