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.
Files changed (105) hide show
  1. {kx_cli-0.0.10/src/kx_cli.egg-info → kx_cli-0.0.11}/PKG-INFO +20 -4
  2. {kx_cli-0.0.10 → kx_cli-0.0.11}/README.md +19 -3
  3. {kx_cli-0.0.10 → kx_cli-0.0.11}/pyproject.toml +1 -1
  4. kx_cli-0.0.11/src/kx/commands/secret.py +58 -0
  5. kx_cli-0.0.11/src/kx/commands/tree.py +65 -0
  6. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/console.py +22 -2
  7. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/graph.py +120 -0
  8. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/main.py +219 -15
  9. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/types.py +2 -0
  10. {kx_cli-0.0.10 → kx_cli-0.0.11/src/kx_cli.egg-info}/PKG-INFO +20 -4
  11. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx_cli.egg-info/SOURCES.txt +3 -0
  12. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_cli_help.py +22 -0
  13. kx_cli-0.0.11/tests/test_cli_secret.py +352 -0
  14. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_graph.py +120 -1
  15. kx_cli-0.0.11/tests/test_secret.py +100 -0
  16. kx_cli-0.0.11/tests/test_tree.py +130 -0
  17. kx_cli-0.0.10/src/kx/commands/tree.py +0 -34
  18. kx_cli-0.0.10/tests/test_tree.py +0 -59
  19. {kx_cli-0.0.10 → kx_cli-0.0.11}/LICENSE +0 -0
  20. {kx_cli-0.0.10 → kx_cli-0.0.11}/setup.cfg +0 -0
  21. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/__init__.py +0 -0
  22. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/__init__.py +0 -0
  23. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/_metadata.py +0 -0
  24. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/annotations.py +0 -0
  25. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/back.py +0 -0
  26. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/context.py +0 -0
  27. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/contexts.py +0 -0
  28. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/delete.py +0 -0
  29. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/describe.py +0 -0
  30. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/diagnostic.py +0 -0
  31. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/drop.py +0 -0
  32. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/edit.py +0 -0
  33. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/events.py +0 -0
  34. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/exec.py +0 -0
  35. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/forward.py +0 -0
  36. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/get.py +0 -0
  37. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/labels.py +0 -0
  38. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/logs.py +0 -0
  39. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/metadata_write.py +0 -0
  40. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/namespace.py +0 -0
  41. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/port_forward.py +0 -0
  42. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/rollout.py +0 -0
  43. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/scale.py +0 -0
  44. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/scan.py +0 -0
  45. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/state.py +0 -0
  46. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/theme.py +0 -0
  47. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/top.py +0 -0
  48. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/commands/yaml.py +0 -0
  49. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/config.py +0 -0
  50. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/diagnostics.py +0 -0
  51. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/errors.py +0 -0
  52. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/events.py +0 -0
  53. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/index.py +0 -0
  54. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/k8s.py +0 -0
  55. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/kinds.py +0 -0
  56. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/kubectl.py +0 -0
  57. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/refresh.py +0 -0
  58. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/scanner.py +0 -0
  59. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/state.py +0 -0
  60. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx/themes.py +0 -0
  61. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx_cli.egg-info/dependency_links.txt +0 -0
  62. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx_cli.egg-info/entry_points.txt +0 -0
  63. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx_cli.egg-info/requires.txt +0 -0
  64. {kx_cli-0.0.10 → kx_cli-0.0.11}/src/kx_cli.egg-info/top_level.txt +0 -0
  65. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_annotations.py +0 -0
  66. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_back.py +0 -0
  67. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_cli_context.py +0 -0
  68. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_cli_get.py +0 -0
  69. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_cli_kind_alias.py +0 -0
  70. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_cli_label_annotate.py +0 -0
  71. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_cli_top.py +0 -0
  72. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_config.py +0 -0
  73. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_console.py +0 -0
  74. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_context.py +0 -0
  75. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_contexts.py +0 -0
  76. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_delete.py +0 -0
  77. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_describe.py +0 -0
  78. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_diagnostic.py +0 -0
  79. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_diagnostics.py +0 -0
  80. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_drop.py +0 -0
  81. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_edit.py +0 -0
  82. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_error_handling.py +0 -0
  83. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_events.py +0 -0
  84. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_exec.py +0 -0
  85. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_forward.py +0 -0
  86. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_get.py +0 -0
  87. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_index.py +0 -0
  88. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_kinds.py +0 -0
  89. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_kubectl.py +0 -0
  90. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_labels.py +0 -0
  91. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_logs.py +0 -0
  92. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_metadata_helper.py +0 -0
  93. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_metadata_write.py +0 -0
  94. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_namespace.py +0 -0
  95. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_port_forward.py +0 -0
  96. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_refresh.py +0 -0
  97. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_rollout.py +0 -0
  98. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_scale.py +0 -0
  99. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_scan.py +0 -0
  100. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_scanner.py +0 -0
  101. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_state.py +0 -0
  102. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_theme.py +0 -0
  103. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_themes.py +0 -0
  104. {kx_cli-0.0.10 → kx_cli-0.0.11}/tests/test_top.py +0 -0
  105. {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.10
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 `--help` on any command shows usage, examples, and aliases.
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 `--help` on any command shows usage, examples, and aliases.
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kx-cli"
3
- version = "0.0.10"
3
+ version = "0.0.11"
4
4
  description = "kubectl wrapper with index-based resource selection"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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`."""