kx-cli 0.0.9__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.9/src/kx_cli.egg-info → kx_cli-0.0.11}/PKG-INFO +21 -5
- {kx_cli-0.0.9 → kx_cli-0.0.11}/README.md +20 -4
- {kx_cli-0.0.9 → kx_cli-0.0.11}/pyproject.toml +1 -1
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/events.py +16 -9
- 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.11/src/kx/commands/yaml.py +43 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/config.py +7 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/console.py +47 -47
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/events.py +16 -1
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/graph.py +120 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/index.py +13 -0
- kx_cli-0.0.11/src/kx/kubectl.py +64 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/main.py +223 -22
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/scanner.py +12 -2
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/state.py +30 -7
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/types.py +2 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11/src/kx_cli.egg-info}/PKG-INFO +21 -5
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx_cli.egg-info/SOURCES.txt +4 -0
- {kx_cli-0.0.9 → 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.9 → kx_cli-0.0.11}/tests/test_config.py +34 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_console.py +82 -15
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_events.py +37 -11
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_graph.py +120 -1
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_index.py +20 -0
- kx_cli-0.0.11/tests/test_kubectl.py +87 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_scanner.py +14 -1
- kx_cli-0.0.11/tests/test_secret.py +100 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_state.py +59 -0
- kx_cli-0.0.11/tests/test_tree.py +130 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_yaml.py +22 -0
- kx_cli-0.0.9/src/kx/commands/tree.py +0 -34
- kx_cli-0.0.9/src/kx/commands/yaml.py +0 -32
- kx_cli-0.0.9/src/kx/kubectl.py +0 -49
- kx_cli-0.0.9/tests/test_tree.py +0 -59
- {kx_cli-0.0.9 → kx_cli-0.0.11}/LICENSE +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/setup.cfg +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/__init__.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/__init__.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/_metadata.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/annotations.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/back.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/context.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/contexts.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/delete.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/describe.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/diagnostic.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/drop.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/edit.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/exec.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/forward.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/get.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/labels.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/logs.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/metadata_write.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/namespace.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/port_forward.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/rollout.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/scale.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/scan.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/state.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/theme.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/commands/top.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/diagnostics.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/errors.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/k8s.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/kinds.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/refresh.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx/themes.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx_cli.egg-info/dependency_links.txt +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx_cli.egg-info/entry_points.txt +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx_cli.egg-info/requires.txt +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/src/kx_cli.egg-info/top_level.txt +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_annotations.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_back.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_cli_context.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_cli_get.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_cli_kind_alias.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_cli_label_annotate.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_cli_top.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_context.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_contexts.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_delete.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_describe.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_diagnostic.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_diagnostics.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_drop.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_edit.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_error_handling.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_exec.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_forward.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_get.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_kinds.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_labels.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_logs.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_metadata_helper.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_metadata_write.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_namespace.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_port_forward.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_refresh.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_rollout.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_scale.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_scan.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_theme.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_themes.py +0 -0
- {kx_cli-0.0.9 → kx_cli-0.0.11}/tests/test_top.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
|
|
@@ -70,7 +70,7 @@ With pip:
|
|
|
70
70
|
pip install kx-cli
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
As a kubectl plugin via [krew](https://krew.sigs.k8s.io/), where kx is published as `idx` (no Python required
|
|
73
|
+
As a kubectl plugin via [krew](https://krew.sigs.k8s.io/), where kx is published as `idx` (no Python required):
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
76
|
kubectl krew install idx
|
|
@@ -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
|
|
@@ -47,7 +47,7 @@ With pip:
|
|
|
47
47
|
pip install kx-cli
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
As a kubectl plugin via [krew](https://krew.sigs.k8s.io/), where kx is published as `idx` (no Python required
|
|
50
|
+
As a kubectl plugin via [krew](https://krew.sigs.k8s.io/), where kx is published as `idx` (no Python required):
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
53
|
kubectl krew install idx
|
|
@@ -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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from kx.events import EventsServiceProtocol
|
|
1
|
+
from kx.events import EventRow, EventsServiceProtocol
|
|
2
2
|
from kx.kubectl import KubectlServiceProtocol
|
|
3
3
|
from kx.refresh import ensure_exists
|
|
4
4
|
from kx.state import StateServiceProtocol
|
|
@@ -15,7 +15,7 @@ class EventsCommand:
|
|
|
15
15
|
self.events = events
|
|
16
16
|
self.kubectl = kubectl
|
|
17
17
|
|
|
18
|
-
def execute(self, index: int) ->
|
|
18
|
+
def execute(self, index: int) -> list[EventRow]:
|
|
19
19
|
name, namespace, kind = self.state.fields(index)
|
|
20
20
|
all_events = self.events.get(namespace)
|
|
21
21
|
filtered = self.events.filter(all_events, name, kind)
|
|
@@ -24,14 +24,21 @@ class EventsCommand:
|
|
|
24
24
|
# Deleted resources keep their events ~1h; only an empty result
|
|
25
25
|
# warrants checking whether the target itself is gone.
|
|
26
26
|
ensure_exists(self.kubectl, kind, name, namespace)
|
|
27
|
-
return
|
|
27
|
+
return []
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
rows = []
|
|
30
30
|
for event in filtered:
|
|
31
31
|
obj = event.involved_object
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
f"{obj.kind:10} {event.metadata.creation_timestamp} "
|
|
35
|
-
f"{event.message}"
|
|
32
|
+
timestamp = getattr(event, "last_timestamp", None) or getattr(
|
|
33
|
+
event.metadata, "creation_timestamp", None
|
|
36
34
|
)
|
|
37
|
-
|
|
35
|
+
rows.append(
|
|
36
|
+
EventRow(
|
|
37
|
+
type=event.type or "",
|
|
38
|
+
reason=event.reason or "",
|
|
39
|
+
kind=obj.kind,
|
|
40
|
+
message=event.message or "",
|
|
41
|
+
timestamp=timestamp,
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
return rows
|
|
@@ -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
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
"""Collect the value of each requested key from anywhere in the manifest,
|
|
9
|
+
preferring the shallowest occurrence when a key appears at multiple depths.
|
|
10
|
+
|
|
11
|
+
A breadth-first walk guarantees shallowest-wins: a workload's own top-level
|
|
12
|
+
`metadata` is returned rather than its pod template's nested `metadata`,
|
|
13
|
+
while genuinely nested-only keys (e.g. `containerStatuses` under `status`)
|
|
14
|
+
are still found. A matched key's own subtree is not descended into."""
|
|
15
|
+
result: dict = {}
|
|
16
|
+
frontier: list = [data]
|
|
17
|
+
while frontier:
|
|
18
|
+
deeper: list = []
|
|
19
|
+
for node in frontier:
|
|
20
|
+
if isinstance(node, dict):
|
|
21
|
+
for k, v in node.items():
|
|
22
|
+
if k in keys:
|
|
23
|
+
result.setdefault(k, v)
|
|
24
|
+
else:
|
|
25
|
+
deeper.append(v)
|
|
26
|
+
elif isinstance(node, list):
|
|
27
|
+
deeper.extend(node)
|
|
28
|
+
frontier = deeper
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class YamlCommand:
|
|
33
|
+
def __init__(self, state: StateServiceProtocol, kubectl: KubectlServiceProtocol):
|
|
34
|
+
self.state = state
|
|
35
|
+
self.kubectl = kubectl
|
|
36
|
+
|
|
37
|
+
def execute(self, index: int, show: list[str] | None = None) -> str:
|
|
38
|
+
name, namespace, kind = self.state.fields(index)
|
|
39
|
+
raw = self.kubectl.run(["get", kind, name, "-n", namespace, "-o", "yaml"])
|
|
40
|
+
if not show:
|
|
41
|
+
return raw
|
|
42
|
+
data = yaml.safe_load(raw)
|
|
43
|
+
return yaml.dump(_find_keys(data, set(show)), default_flow_style=False)
|
|
@@ -52,6 +52,13 @@ def load_config() -> Config:
|
|
|
52
52
|
if "KX_THEME" in os.environ:
|
|
53
53
|
kwargs["theme"] = os.environ["KX_THEME"]
|
|
54
54
|
|
|
55
|
+
if "max_history" in kwargs:
|
|
56
|
+
mh = kwargs["max_history"]
|
|
57
|
+
# bool is an int subclass; reject it and any non-int/non-positive value
|
|
58
|
+
# (a string from TOML, 0, or a negative) with a clear message.
|
|
59
|
+
if isinstance(mh, bool) or not isinstance(mh, int) or mh < 1:
|
|
60
|
+
raise SystemExit("kx: max_history must be a positive integer")
|
|
61
|
+
|
|
55
62
|
if "theme" in kwargs and kwargs["theme"] not in THEMES:
|
|
56
63
|
raise SystemExit(
|
|
57
64
|
f"kx: unknown theme '{kwargs['theme']}' (run kx theme to list themes)"
|
|
@@ -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
|
|
@@ -14,6 +15,8 @@ from rich.table import Table
|
|
|
14
15
|
from rich.text import Text
|
|
15
16
|
|
|
16
17
|
from kx.diagnostics import SEVERITY_PATTERN, Severity
|
|
18
|
+
from kx.events import EventRow
|
|
19
|
+
from kx.index import _parse_output
|
|
17
20
|
from kx.kinds import plural_display
|
|
18
21
|
from kx.scanner import SEVERITIES, ImageScan
|
|
19
22
|
from kx.state import StateHistory
|
|
@@ -192,6 +195,25 @@ def print_raw(text: str) -> None:
|
|
|
192
195
|
_console.print(text, markup=False, highlight=False)
|
|
193
196
|
|
|
194
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
|
+
|
|
195
217
|
def print_rich(renderable) -> None:
|
|
196
218
|
_console.print(renderable)
|
|
197
219
|
|
|
@@ -245,28 +267,20 @@ def _print_get_caption(resource_type: str, namespace: str, count: int) -> None:
|
|
|
245
267
|
def render_indexed_table(
|
|
246
268
|
text: str, resource_type: str, namespace: str, note: str | None = None
|
|
247
269
|
) -> None:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
270
|
+
# Share index._parse_output (the single table parser) rather than a private
|
|
271
|
+
# copy: it extends the last column to end-of-line, so a value wider than its
|
|
272
|
+
# header (kubectl doesn't pad the last column) is never sliced off.
|
|
273
|
+
headers, rows, _ = _parse_output(text)
|
|
274
|
+
if not headers:
|
|
275
|
+
# Non-tabular output (JSON/YAML, or a table with no NAME column) prints
|
|
276
|
+
# as-is; genuinely empty stdout (kubectl sends "No resources found" to
|
|
277
|
+
# stderr) shows the zero-count caption instead of silence.
|
|
278
|
+
if text.strip():
|
|
279
|
+
_console.print(text, markup=False, highlight=False)
|
|
280
|
+
else:
|
|
281
|
+
_print_get_caption(resource_type, namespace, 0)
|
|
259
282
|
return
|
|
260
283
|
|
|
261
|
-
spans = [(m.start(), m.end()) for m in re.finditer(r"\S+\s*", header_line)]
|
|
262
|
-
headers = [header_line[start:end].strip() for start, end in spans]
|
|
263
|
-
|
|
264
|
-
rows = []
|
|
265
|
-
for line in lines[1:]:
|
|
266
|
-
cols = [line[start:end].strip() for start, end in spans]
|
|
267
|
-
if cols:
|
|
268
|
-
rows.append(cols)
|
|
269
|
-
|
|
270
284
|
if not rows:
|
|
271
285
|
_print_get_caption(resource_type, namespace, 0)
|
|
272
286
|
return
|
|
@@ -329,8 +343,8 @@ def render_indexed_table(
|
|
|
329
343
|
_console.print(f"[muted]{note}[/muted]")
|
|
330
344
|
|
|
331
345
|
|
|
332
|
-
def render_events_table(
|
|
333
|
-
if
|
|
346
|
+
def render_events_table(rows: list[EventRow]) -> None:
|
|
347
|
+
if not rows:
|
|
334
348
|
_console.print("[muted]No events found[/muted]")
|
|
335
349
|
return
|
|
336
350
|
|
|
@@ -343,31 +357,17 @@ def render_events_table(text: str) -> None:
|
|
|
343
357
|
for col in ("TYPE", "REASON", "KIND", "AGE", "MESSAGE"):
|
|
344
358
|
table.add_column(col)
|
|
345
359
|
|
|
346
|
-
for
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
event_type = line[0:8].strip()
|
|
350
|
-
reason = line[9:39].strip()
|
|
351
|
-
kind = line[40:50].strip()
|
|
352
|
-
rest = line[51:]
|
|
353
|
-
parts = rest.split(" ", 2)
|
|
354
|
-
timestamp = (
|
|
355
|
-
f"{parts[0]} {parts[1]}" if len(parts) >= 2 else (parts[0] if parts else "")
|
|
356
|
-
)
|
|
357
|
-
message = parts[2] if len(parts) >= 3 else ""
|
|
358
|
-
|
|
359
|
-
try:
|
|
360
|
-
age = _format_age(datetime.fromisoformat(timestamp))
|
|
361
|
-
except ValueError:
|
|
362
|
-
age = timestamp
|
|
363
|
-
|
|
364
|
-
type_color = "muted" if event_type == "Normal" else "warn"
|
|
360
|
+
for row in rows:
|
|
361
|
+
age = _format_age(row.timestamp)
|
|
362
|
+
type_color = "muted" if row.type == "Normal" else "warn"
|
|
365
363
|
table.add_row(
|
|
366
|
-
f"[{type_color}]{
|
|
367
|
-
reason,
|
|
368
|
-
kind,
|
|
364
|
+
f"[{type_color}]{row.type}[/]",
|
|
365
|
+
row.reason,
|
|
366
|
+
row.kind,
|
|
369
367
|
f"[muted]{age}[/]",
|
|
370
|
-
message
|
|
368
|
+
# Text (not markup) so bracketed characters in a message are shown
|
|
369
|
+
# literally, matching the WARNING EVENTS section.
|
|
370
|
+
Text(row.message),
|
|
371
371
|
)
|
|
372
372
|
|
|
373
373
|
_console.print(table)
|
|
@@ -687,7 +687,7 @@ def print_command_help(ctx) -> None:
|
|
|
687
687
|
names = " ".join(opt.opts)
|
|
688
688
|
_console.print(f" [body]{names:<20}[/body] [muted]{opt.help or ''}[/muted]")
|
|
689
689
|
_console.print(
|
|
690
|
-
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]"
|
|
691
691
|
)
|
|
692
692
|
|
|
693
693
|
aliases = getattr(ctx.command.callback, "_aliases", [])
|
|
@@ -745,7 +745,7 @@ def print_help(
|
|
|
745
745
|
f" [body]{'-v, --version':<14}[/body] [muted]Show the kx version and exit.[/muted]"
|
|
746
746
|
)
|
|
747
747
|
_console.print(
|
|
748
|
-
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]"
|
|
749
749
|
)
|
|
750
750
|
if version:
|
|
751
751
|
_console.print()
|
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
from
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
2
3
|
from typing import Protocol
|
|
3
4
|
|
|
5
|
+
from kubernetes import client
|
|
6
|
+
|
|
4
7
|
from kx.k8s import load_config
|
|
5
8
|
|
|
6
9
|
|
|
10
|
+
@dataclass
|
|
11
|
+
class EventRow:
|
|
12
|
+
"""One event, flattened for rendering. `timestamp` is the event's last-seen
|
|
13
|
+
time (falling back to creation) as a datetime, or None if unavailable."""
|
|
14
|
+
|
|
15
|
+
type: str
|
|
16
|
+
reason: str
|
|
17
|
+
kind: str
|
|
18
|
+
message: str
|
|
19
|
+
timestamp: datetime | None = None
|
|
20
|
+
|
|
21
|
+
|
|
7
22
|
class EventsServiceProtocol(Protocol):
|
|
8
23
|
def get(self, namespace: str) -> list: ...
|
|
9
24
|
def filter(self, events: list, name: str, kind: str) -> list: ...
|
|
@@ -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`."""
|