kube-query 0.1.0__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.
- kube_query-0.1.0/LICENSE +21 -0
- kube_query-0.1.0/PKG-INFO +160 -0
- kube_query-0.1.0/README.md +135 -0
- kube_query-0.1.0/pyproject.toml +75 -0
- kube_query-0.1.0/setup.cfg +4 -0
- kube_query-0.1.0/src/kube_query/__init__.py +3 -0
- kube_query-0.1.0/src/kube_query/access.py +85 -0
- kube_query-0.1.0/src/kube_query/cleanup.py +115 -0
- kube_query-0.1.0/src/kube_query/cli.py +81 -0
- kube_query-0.1.0/src/kube_query/cli_kit.py +175 -0
- kube_query-0.1.0/src/kube_query/commands/__init__.py +92 -0
- kube_query-0.1.0/src/kube_query/commands/_impl.py +147 -0
- kube_query-0.1.0/src/kube_query/commands/api_resources.py +66 -0
- kube_query-0.1.0/src/kube_query/commands/contexts.py +94 -0
- kube_query-0.1.0/src/kube_query/commands/describe.py +168 -0
- kube_query-0.1.0/src/kube_query/commands/eso_cmd.py +44 -0
- kube_query-0.1.0/src/kube_query/commands/events_cmd.py +67 -0
- kube_query-0.1.0/src/kube_query/commands/get.py +52 -0
- kube_query-0.1.0/src/kube_query/commands/groups.py +166 -0
- kube_query-0.1.0/src/kube_query/commands/helm_cmd.py +109 -0
- kube_query-0.1.0/src/kube_query/commands/logs.py +73 -0
- kube_query-0.1.0/src/kube_query/commands/overview.py +347 -0
- kube_query-0.1.0/src/kube_query/commands/tree_cmd.py +47 -0
- kube_query-0.1.0/src/kube_query/commands/version.py +57 -0
- kube_query-0.1.0/src/kube_query/config.py +210 -0
- kube_query-0.1.0/src/kube_query/discovery.py +191 -0
- kube_query-0.1.0/src/kube_query/errors.py +45 -0
- kube_query-0.1.0/src/kube_query/eso.py +81 -0
- kube_query-0.1.0/src/kube_query/events.py +104 -0
- kube_query-0.1.0/src/kube_query/helm.py +260 -0
- kube_query-0.1.0/src/kube_query/http.py +101 -0
- kube_query-0.1.0/src/kube_query/output.py +215 -0
- kube_query-0.1.0/src/kube_query/py.typed +0 -0
- kube_query-0.1.0/src/kube_query/summarize.py +391 -0
- kube_query-0.1.0/src/kube_query/tree.py +210 -0
- kube_query-0.1.0/src/kube_query.egg-info/PKG-INFO +160 -0
- kube_query-0.1.0/src/kube_query.egg-info/SOURCES.txt +45 -0
- kube_query-0.1.0/src/kube_query.egg-info/dependency_links.txt +1 -0
- kube_query-0.1.0/src/kube_query.egg-info/entry_points.txt +3 -0
- kube_query-0.1.0/src/kube_query.egg-info/requires.txt +7 -0
- kube_query-0.1.0/src/kube_query.egg-info/top_level.txt +1 -0
- kube_query-0.1.0/tests/test_cleanup.py +79 -0
- kube_query-0.1.0/tests/test_discovery.py +136 -0
- kube_query-0.1.0/tests/test_helm.py +101 -0
- kube_query-0.1.0/tests/test_output.py +55 -0
- kube_query-0.1.0/tests/test_smoke.py +450 -0
- kube_query-0.1.0/tests/test_summarize.py +119 -0
kube_query-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kube-query contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kube-query
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read-only Kubernetes query CLI - compact YAML/JSON/rich/markdown output, built for humans and AI agents
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: kubernetes,kubectl,cli,query,read-only,ai-agents,troubleshooting,helm,eks,external-secrets
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: System Administrators
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Topic :: System :: Clustering
|
|
14
|
+
Classifier: Topic :: System :: Systems Administration
|
|
15
|
+
Requires-Python: >=3.13
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: click>=8.1
|
|
19
|
+
Requires-Dist: kubernetes>=28.1.0
|
|
20
|
+
Requires-Dist: pyyaml>=6.0
|
|
21
|
+
Requires-Dist: rich>=13.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# kube-query
|
|
27
|
+
|
|
28
|
+
**`kq` — read-only Kubernetes queries for humans and AI agents.**
|
|
29
|
+
|
|
30
|
+
kubectl is a loaded gun in an agent's hands. kube-query is the safe scope:
|
|
31
|
+
it **captures cluster state and diagnoses problems**, and *cannot* create,
|
|
32
|
+
update or delete anything — the HTTP layer implements `GET` and nothing else.
|
|
33
|
+
Compact YAML by default, with rich tables, markdown, JSON and ownership
|
|
34
|
+
trees on request. Built-ins, shortnames, CRDs, Helm releases and External
|
|
35
|
+
Secrets Operator — one small, fast binary-ish CLI.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
kq p l -A # all pods, all namespaces
|
|
39
|
+
kq p desc web-5d9c6 -n prod # status + reasons + restarts + events, one doc
|
|
40
|
+
kq logs web-5d9c6 -n prod -c app --previous
|
|
41
|
+
kq tree deploy/web -n prod # Deployment -> ReplicaSet -> Pods, health per node
|
|
42
|
+
kq overview # whole-cluster snapshot, problems first
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> kq is the **diagnosis** half of the loop. The **fix** half belongs to other
|
|
46
|
+
> tools (kubectl apply, gitops, helm) — by design.
|
|
47
|
+
|
|
48
|
+
## Why
|
|
49
|
+
|
|
50
|
+
| Problem with `kubectl` for agents | kube-query |
|
|
51
|
+
|---|---|
|
|
52
|
+
| can `delete`/`scale`/`apply` — one hallucinated flag away from an incident | GET-only; no mutating code path exists |
|
|
53
|
+
| `-o yaml` dumps managedFields/uid/annotations noise into context | compact by default, `-v`/`-vv` for more |
|
|
54
|
+
| output shaped for terminals, not reasoning | yaml/json data, md reports, tree views |
|
|
55
|
+
| 100+ verbs to misuse | ~15 read verbs with aliases (`kq p l`, `kq d l`, `kq c l`) |
|
|
56
|
+
| helm/ESO/CRD troubleshooting needs extra tools | helm releases, ESO status and CRDs built in |
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
Requires Python 3.13+.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pipx install kube-query # or: uv tool install kube-query
|
|
64
|
+
kq ctx ls # verify — reads your existing kubeconfig
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
From source:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git clone <repo> && cd kube-query
|
|
71
|
+
make setup # .venv (py3.13) + dev deps
|
|
72
|
+
.venv/bin/kq overview
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
No cluster to try it on? `make demo` boots a fake API server with broken
|
|
76
|
+
pods, a stuck deployment, helm releases and ESO resources to poke at.
|
|
77
|
+
|
|
78
|
+
## Commands
|
|
79
|
+
|
|
80
|
+
| Command (aliases) | Purpose |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `kq overview` (`ov`, `o`, `health`) | cluster snapshot: problem pods, nodes, deployments, PVCs, warnings |
|
|
83
|
+
| `kq pod` (`pods`, `p`, `po`) | `l [REGEX] [-A\|-n\|-l\|--field-selector]` · `NAME` · `desc NAME` · `logs NAME` |
|
|
84
|
+
| `kq deploy` (`deployments`, `dep`, `d`) | same shape for deployments |
|
|
85
|
+
| `kq ns` (`n`) · `kq node` (`no`) · `kq svc` · `kq cm` | namespaces, nodes, services, configmaps |
|
|
86
|
+
| `kq secret` (`sec`) | values hidden; `kq sec NAME --reveal` decodes (explicit opt-in) |
|
|
87
|
+
| `kq ev` (`events`, `e`) | events, newest first: `--type Warning --since 2h --for pod/NAME` |
|
|
88
|
+
| `kq get RESOURCE[/NAME]` (`g`) | any resource incl. CRDs; `kq api` lists all types |
|
|
89
|
+
| `kq describe` (`desc`) | diagnosis bundle: summary + object + children + events |
|
|
90
|
+
| `kq tree RESOURCE/NAME` (`t`) | ownership tree (`deploy/web`, `cronjob/x`, `node/y`) |
|
|
91
|
+
| `kq helm` (`h`) | releases read-only from Helm's release Secrets; `show NAME` adds values |
|
|
92
|
+
| `kq eso` | ExternalSecret sync status (`--stores` for SecretStores) |
|
|
93
|
+
| `kq ctx` (`c`) | kubeconfig contexts (`kq c l`), no API calls |
|
|
94
|
+
| `kq ver` | client + server version (which cluster am I on?) |
|
|
95
|
+
|
|
96
|
+
Shared flags: `-n NS` · `-A` · `-l SELECTOR` · `--field-selector` · `-f/--filter REGEX`
|
|
97
|
+
· `-o yaml|json|rich|md|tree` (default yaml, `KQ_OUTPUT` env) · `-v`/`-vv`.
|
|
98
|
+
|
|
99
|
+
## Output
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
kq overview -o rich # aligned, colored tables (humans)
|
|
103
|
+
kq p l -A -o md # markdown tables (tickets, chat, agent reports)
|
|
104
|
+
kq p web-5d9c6 -o json # machine-readable
|
|
105
|
+
kq d l -o tree # ownership view
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Stdout is always the payload (parseable); commentary goes to stderr as
|
|
109
|
+
`#`-prefixed notes. Exit codes: `0` ok, `1` error (one actionable sentence),
|
|
110
|
+
`2` usage.
|
|
111
|
+
|
|
112
|
+
## For AI agents
|
|
113
|
+
|
|
114
|
+
Give your agent (Claude Code, opencode, codex, …) the one-file skill:
|
|
115
|
+
[`SKILL.md`](SKILL.md) — command map, the 5-move triage loop, output
|
|
116
|
+
contract, failure-signature table, and rules of engagement. Suggested
|
|
117
|
+
system-prompt line:
|
|
118
|
+
|
|
119
|
+
> Use `kq` (read-only) to inspect the Kubernetes cluster: start with
|
|
120
|
+
> `kq overview`, then `kq p desc <pod>`, `kq logs`, `kq tree`, `kq ev`.
|
|
121
|
+
> Never attempt cluster changes with kq; propose them for kubectl instead.
|
|
122
|
+
|
|
123
|
+
## Documentation
|
|
124
|
+
|
|
125
|
+
| Doc | Contents |
|
|
126
|
+
|---|---|
|
|
127
|
+
| [docs/usage.md](docs/usage.md) | full manual: every command, flag, format, exit code |
|
|
128
|
+
| [docs/tutorial.md](docs/tutorial.md) | hands-on: debugging a broken deployment end-to-end |
|
|
129
|
+
| [docs/architecture.md](docs/architecture.md) | module map + data flow diagrams |
|
|
130
|
+
| [docs/extending.md](docs/extending.md) | add commands, columns, formats (with examples) |
|
|
131
|
+
| [docs/roadmap.md](docs/roadmap.md) | what's next + explicit non-goals |
|
|
132
|
+
| [CHANGELOG.md](CHANGELOG.md) | releases |
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
make setup # venv + deps
|
|
138
|
+
make check # the full gate: ruff lint + format check + pytest + build
|
|
139
|
+
make demo # fake-cluster playground
|
|
140
|
+
make tag # run the gate, then create v<version> tag
|
|
141
|
+
git push origin vX.Y.Z # CI publishes to PyPI (trusted publishing)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Pipeline: `make check` mirrors CI exactly; pushing a `v*` tag runs the same
|
|
145
|
+
gate, builds sdist+wheel, verifies tag/version match, and publishes via
|
|
146
|
+
GitHub Actions to PyPI — no stored secrets (OIDC trusted publishing).
|
|
147
|
+
|
|
148
|
+
Tests are network-free: unit suites plus smoke tests that drive the real CLI
|
|
149
|
+
against an in-process fake Kubernetes API server.
|
|
150
|
+
|
|
151
|
+
## Safety model
|
|
152
|
+
|
|
153
|
+
1. `kube_query/http.py` exposes `raw_get` only — there is no verb parameter anywhere.
|
|
154
|
+
2. `kube_query/access.py` builds list/get/log paths only.
|
|
155
|
+
3. Secret values are redacted unless `--reveal` is passed explicitly.
|
|
156
|
+
4. Your RBAC still applies — kq reads exactly what your credentials allow.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# kube-query
|
|
2
|
+
|
|
3
|
+
**`kq` — read-only Kubernetes queries for humans and AI agents.**
|
|
4
|
+
|
|
5
|
+
kubectl is a loaded gun in an agent's hands. kube-query is the safe scope:
|
|
6
|
+
it **captures cluster state and diagnoses problems**, and *cannot* create,
|
|
7
|
+
update or delete anything — the HTTP layer implements `GET` and nothing else.
|
|
8
|
+
Compact YAML by default, with rich tables, markdown, JSON and ownership
|
|
9
|
+
trees on request. Built-ins, shortnames, CRDs, Helm releases and External
|
|
10
|
+
Secrets Operator — one small, fast binary-ish CLI.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
kq p l -A # all pods, all namespaces
|
|
14
|
+
kq p desc web-5d9c6 -n prod # status + reasons + restarts + events, one doc
|
|
15
|
+
kq logs web-5d9c6 -n prod -c app --previous
|
|
16
|
+
kq tree deploy/web -n prod # Deployment -> ReplicaSet -> Pods, health per node
|
|
17
|
+
kq overview # whole-cluster snapshot, problems first
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
> kq is the **diagnosis** half of the loop. The **fix** half belongs to other
|
|
21
|
+
> tools (kubectl apply, gitops, helm) — by design.
|
|
22
|
+
|
|
23
|
+
## Why
|
|
24
|
+
|
|
25
|
+
| Problem with `kubectl` for agents | kube-query |
|
|
26
|
+
|---|---|
|
|
27
|
+
| can `delete`/`scale`/`apply` — one hallucinated flag away from an incident | GET-only; no mutating code path exists |
|
|
28
|
+
| `-o yaml` dumps managedFields/uid/annotations noise into context | compact by default, `-v`/`-vv` for more |
|
|
29
|
+
| output shaped for terminals, not reasoning | yaml/json data, md reports, tree views |
|
|
30
|
+
| 100+ verbs to misuse | ~15 read verbs with aliases (`kq p l`, `kq d l`, `kq c l`) |
|
|
31
|
+
| helm/ESO/CRD troubleshooting needs extra tools | helm releases, ESO status and CRDs built in |
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
Requires Python 3.13+.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pipx install kube-query # or: uv tool install kube-query
|
|
39
|
+
kq ctx ls # verify — reads your existing kubeconfig
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
From source:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone <repo> && cd kube-query
|
|
46
|
+
make setup # .venv (py3.13) + dev deps
|
|
47
|
+
.venv/bin/kq overview
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
No cluster to try it on? `make demo` boots a fake API server with broken
|
|
51
|
+
pods, a stuck deployment, helm releases and ESO resources to poke at.
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
| Command (aliases) | Purpose |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `kq overview` (`ov`, `o`, `health`) | cluster snapshot: problem pods, nodes, deployments, PVCs, warnings |
|
|
58
|
+
| `kq pod` (`pods`, `p`, `po`) | `l [REGEX] [-A\|-n\|-l\|--field-selector]` · `NAME` · `desc NAME` · `logs NAME` |
|
|
59
|
+
| `kq deploy` (`deployments`, `dep`, `d`) | same shape for deployments |
|
|
60
|
+
| `kq ns` (`n`) · `kq node` (`no`) · `kq svc` · `kq cm` | namespaces, nodes, services, configmaps |
|
|
61
|
+
| `kq secret` (`sec`) | values hidden; `kq sec NAME --reveal` decodes (explicit opt-in) |
|
|
62
|
+
| `kq ev` (`events`, `e`) | events, newest first: `--type Warning --since 2h --for pod/NAME` |
|
|
63
|
+
| `kq get RESOURCE[/NAME]` (`g`) | any resource incl. CRDs; `kq api` lists all types |
|
|
64
|
+
| `kq describe` (`desc`) | diagnosis bundle: summary + object + children + events |
|
|
65
|
+
| `kq tree RESOURCE/NAME` (`t`) | ownership tree (`deploy/web`, `cronjob/x`, `node/y`) |
|
|
66
|
+
| `kq helm` (`h`) | releases read-only from Helm's release Secrets; `show NAME` adds values |
|
|
67
|
+
| `kq eso` | ExternalSecret sync status (`--stores` for SecretStores) |
|
|
68
|
+
| `kq ctx` (`c`) | kubeconfig contexts (`kq c l`), no API calls |
|
|
69
|
+
| `kq ver` | client + server version (which cluster am I on?) |
|
|
70
|
+
|
|
71
|
+
Shared flags: `-n NS` · `-A` · `-l SELECTOR` · `--field-selector` · `-f/--filter REGEX`
|
|
72
|
+
· `-o yaml|json|rich|md|tree` (default yaml, `KQ_OUTPUT` env) · `-v`/`-vv`.
|
|
73
|
+
|
|
74
|
+
## Output
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
kq overview -o rich # aligned, colored tables (humans)
|
|
78
|
+
kq p l -A -o md # markdown tables (tickets, chat, agent reports)
|
|
79
|
+
kq p web-5d9c6 -o json # machine-readable
|
|
80
|
+
kq d l -o tree # ownership view
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Stdout is always the payload (parseable); commentary goes to stderr as
|
|
84
|
+
`#`-prefixed notes. Exit codes: `0` ok, `1` error (one actionable sentence),
|
|
85
|
+
`2` usage.
|
|
86
|
+
|
|
87
|
+
## For AI agents
|
|
88
|
+
|
|
89
|
+
Give your agent (Claude Code, opencode, codex, …) the one-file skill:
|
|
90
|
+
[`SKILL.md`](SKILL.md) — command map, the 5-move triage loop, output
|
|
91
|
+
contract, failure-signature table, and rules of engagement. Suggested
|
|
92
|
+
system-prompt line:
|
|
93
|
+
|
|
94
|
+
> Use `kq` (read-only) to inspect the Kubernetes cluster: start with
|
|
95
|
+
> `kq overview`, then `kq p desc <pod>`, `kq logs`, `kq tree`, `kq ev`.
|
|
96
|
+
> Never attempt cluster changes with kq; propose them for kubectl instead.
|
|
97
|
+
|
|
98
|
+
## Documentation
|
|
99
|
+
|
|
100
|
+
| Doc | Contents |
|
|
101
|
+
|---|---|
|
|
102
|
+
| [docs/usage.md](docs/usage.md) | full manual: every command, flag, format, exit code |
|
|
103
|
+
| [docs/tutorial.md](docs/tutorial.md) | hands-on: debugging a broken deployment end-to-end |
|
|
104
|
+
| [docs/architecture.md](docs/architecture.md) | module map + data flow diagrams |
|
|
105
|
+
| [docs/extending.md](docs/extending.md) | add commands, columns, formats (with examples) |
|
|
106
|
+
| [docs/roadmap.md](docs/roadmap.md) | what's next + explicit non-goals |
|
|
107
|
+
| [CHANGELOG.md](CHANGELOG.md) | releases |
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
make setup # venv + deps
|
|
113
|
+
make check # the full gate: ruff lint + format check + pytest + build
|
|
114
|
+
make demo # fake-cluster playground
|
|
115
|
+
make tag # run the gate, then create v<version> tag
|
|
116
|
+
git push origin vX.Y.Z # CI publishes to PyPI (trusted publishing)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Pipeline: `make check` mirrors CI exactly; pushing a `v*` tag runs the same
|
|
120
|
+
gate, builds sdist+wheel, verifies tag/version match, and publishes via
|
|
121
|
+
GitHub Actions to PyPI — no stored secrets (OIDC trusted publishing).
|
|
122
|
+
|
|
123
|
+
Tests are network-free: unit suites plus smoke tests that drive the real CLI
|
|
124
|
+
against an in-process fake Kubernetes API server.
|
|
125
|
+
|
|
126
|
+
## Safety model
|
|
127
|
+
|
|
128
|
+
1. `kube_query/http.py` exposes `raw_get` only — there is no verb parameter anywhere.
|
|
129
|
+
2. `kube_query/access.py` builds list/get/log paths only.
|
|
130
|
+
3. Secret values are redacted unless `--reveal` is passed explicitly.
|
|
131
|
+
4. Your RBAC still applies — kq reads exactly what your credentials allow.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kube-query"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Read-only Kubernetes query CLI - compact YAML/JSON/rich/markdown output, built for humans and AI agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.13"
|
|
13
|
+
keywords = ["kubernetes", "kubectl", "cli", "query", "read-only", "ai-agents", "troubleshooting", "helm", "eks", "external-secrets"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: System Administrators",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: System :: Clustering",
|
|
22
|
+
"Topic :: System :: Systems Administration",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"click>=8.1",
|
|
26
|
+
"kubernetes>=28.1.0",
|
|
27
|
+
"pyyaml>=6.0",
|
|
28
|
+
"rich>=13.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["pytest>=8"]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
kube-query = "kube_query.cli:main"
|
|
36
|
+
kq = "kube_query.cli:main"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["src"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
kube_query = ["py.typed"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
addopts = "-q"
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 120
|
|
50
|
+
target-version = "py313"
|
|
51
|
+
src = ["src", "tests"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint]
|
|
54
|
+
select = [
|
|
55
|
+
"E", # pycodestyle errors
|
|
56
|
+
"W", # pycodestyle warnings
|
|
57
|
+
"F", # pyflakes
|
|
58
|
+
"I", # isort
|
|
59
|
+
"UP", # pyupgrade
|
|
60
|
+
"B", # flake8-bugbear
|
|
61
|
+
"C4", # flake8-comprehensions
|
|
62
|
+
"SIM", # flake8-simplify
|
|
63
|
+
"RUF", # ruff-specific
|
|
64
|
+
]
|
|
65
|
+
ignore = [
|
|
66
|
+
"E501", # long lines handled by formatter
|
|
67
|
+
"SIM108", # ternary rewrites hurt readability here
|
|
68
|
+
"SIM105", # contextlib.suppress rewrites hurt readability
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.ruff.lint.per-file-ignores]
|
|
72
|
+
"tests/*" = ["E402"] # conftest-style imports after sys.path tweaks
|
|
73
|
+
|
|
74
|
+
[tool.ruff.format]
|
|
75
|
+
quote-style = "double"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Object-level access: list/get any resource by its ApiResource, read pod logs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
from .config import Ctx
|
|
8
|
+
from .discovery import ApiResource
|
|
9
|
+
from .errors import KqApiError, KqError
|
|
10
|
+
from .http import get_json, get_text
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def resource_path(entry: ApiResource, namespace: str | None = None, name: str | None = None) -> str:
|
|
14
|
+
if entry.group:
|
|
15
|
+
base = f"/apis/{quote(entry.api_version, safe='/')}"
|
|
16
|
+
else:
|
|
17
|
+
base = f"/api/{entry.version}"
|
|
18
|
+
if entry.namespaced and namespace:
|
|
19
|
+
base += f"/namespaces/{quote(namespace, safe='')}"
|
|
20
|
+
base += f"/{entry.plural}"
|
|
21
|
+
if name:
|
|
22
|
+
base += f"/{quote(name, safe='')}"
|
|
23
|
+
return base
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def list_objects(
|
|
27
|
+
ctx: Ctx,
|
|
28
|
+
entry: ApiResource,
|
|
29
|
+
*,
|
|
30
|
+
namespace: str | None = None,
|
|
31
|
+
label_selector: str | None = None,
|
|
32
|
+
field_selector: str | None = None,
|
|
33
|
+
) -> list[dict]:
|
|
34
|
+
"""List objects. ``namespace=None`` on a namespaced resource lists all namespaces."""
|
|
35
|
+
if not entry.namespaced and namespace:
|
|
36
|
+
namespace = None
|
|
37
|
+
params = {"labelSelector": label_selector, "fieldSelector": field_selector}
|
|
38
|
+
data = get_json(ctx.api_client, resource_path(entry, namespace), params)
|
|
39
|
+
items = data.get("items")
|
|
40
|
+
if not isinstance(items, list):
|
|
41
|
+
raise KqError(f"unexpected response while listing {entry.plural}")
|
|
42
|
+
return items
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_object(ctx: Ctx, entry: ApiResource, name: str, *, namespace: str | None = None) -> dict:
|
|
46
|
+
if entry.namespaced and not namespace:
|
|
47
|
+
raise KqError(f"internal: namespace required to fetch a namespaced {entry.kind}")
|
|
48
|
+
try:
|
|
49
|
+
return get_json(ctx.api_client, resource_path(entry, namespace, name))
|
|
50
|
+
except KqApiError as e:
|
|
51
|
+
if e.status == 404:
|
|
52
|
+
where = f" in namespace '{namespace}'" if entry.namespaced else ""
|
|
53
|
+
scope = f" -n {namespace}" if entry.namespaced else ""
|
|
54
|
+
alt = " (or -A for all namespaces)" if entry.namespaced else ""
|
|
55
|
+
raise KqError(
|
|
56
|
+
f"{entry.kind} '{name}' not found{where}.\n"
|
|
57
|
+
f" Hint: list candidates with 'kq get {entry.plural}{scope}'{alt}."
|
|
58
|
+
) from None
|
|
59
|
+
raise
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def pod_logs(
|
|
63
|
+
ctx: Ctx,
|
|
64
|
+
namespace: str,
|
|
65
|
+
pod: str,
|
|
66
|
+
*,
|
|
67
|
+
container: str | None = None,
|
|
68
|
+
tail: int | None = None,
|
|
69
|
+
previous: bool = False,
|
|
70
|
+
since_seconds: int | None = None,
|
|
71
|
+
timestamps: bool = False,
|
|
72
|
+
) -> str:
|
|
73
|
+
path = f"/api/v1/namespaces/{quote(namespace, safe='')}/pods/{quote(pod, safe='')}/log"
|
|
74
|
+
params = {
|
|
75
|
+
"container": container,
|
|
76
|
+
"tailLines": tail,
|
|
77
|
+
"previous": "true" if previous else None,
|
|
78
|
+
"sinceSeconds": since_seconds,
|
|
79
|
+
"timestamps": "true" if timestamps else None,
|
|
80
|
+
}
|
|
81
|
+
return get_text(ctx.api_client, path, params)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def server_version(ctx: Ctx) -> dict:
|
|
85
|
+
return get_json(ctx.api_client, "/version")
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Output compaction: strip agent-irrelevant noise from Kubernetes objects.
|
|
2
|
+
|
|
3
|
+
Verbosity levels::
|
|
4
|
+
|
|
5
|
+
0 (default) drop managedFields, uid, resourceVersion, generation,
|
|
6
|
+
selfLink, noisy annotations, and all empty values
|
|
7
|
+
1 (-v) drop managedFields and noisy annotations only
|
|
8
|
+
2 (-vv) raw object, nothing is stripped
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import base64
|
|
14
|
+
|
|
15
|
+
NOISY_META = ("managedFields", "selfLink", "resourceVersion", "uid", "generation")
|
|
16
|
+
NOISY_ANNOTATIONS = (
|
|
17
|
+
"kubectl.kubernetes.io/last-applied-configuration",
|
|
18
|
+
"deployment.kubernetes.io/revision",
|
|
19
|
+
"deprecated.daemonset.template.generation",
|
|
20
|
+
)
|
|
21
|
+
_EMPTY = ("", {}, [], None)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def compact(obj: dict | list, verbose: int = 0) -> dict | list:
|
|
25
|
+
if verbose >= 2:
|
|
26
|
+
return obj
|
|
27
|
+
obj = _strip_noisy(obj)
|
|
28
|
+
if verbose == 0:
|
|
29
|
+
obj = _drop_empty(obj)
|
|
30
|
+
return obj
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _strip_noisy(node):
|
|
34
|
+
if isinstance(node, dict):
|
|
35
|
+
out = {}
|
|
36
|
+
for k, v in node.items():
|
|
37
|
+
if k == "metadata" and isinstance(v, dict):
|
|
38
|
+
v = {mk: mv for mk, mv in v.items() if mk != "managedFields"}
|
|
39
|
+
ann = v.get("annotations")
|
|
40
|
+
if isinstance(ann, dict):
|
|
41
|
+
ann = {a: b for a, b in ann.items() if a not in NOISY_ANNOTATIONS}
|
|
42
|
+
if ann:
|
|
43
|
+
v["annotations"] = ann
|
|
44
|
+
else:
|
|
45
|
+
v.pop("annotations", None)
|
|
46
|
+
out[k] = _strip_noisy(v)
|
|
47
|
+
continue
|
|
48
|
+
out[k] = _strip_noisy(v)
|
|
49
|
+
return out
|
|
50
|
+
if isinstance(node, list):
|
|
51
|
+
return [_strip_noisy(x) for x in node]
|
|
52
|
+
return node
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _drop_empty(node):
|
|
56
|
+
if isinstance(node, dict):
|
|
57
|
+
out = {}
|
|
58
|
+
for k, v in node.items():
|
|
59
|
+
if k == "metadata" and isinstance(v, dict):
|
|
60
|
+
v = {mk: mv for mk, mv in v.items() if mk not in NOISY_META}
|
|
61
|
+
v = _drop_empty(v)
|
|
62
|
+
if v in _EMPTY:
|
|
63
|
+
continue
|
|
64
|
+
out[k] = v
|
|
65
|
+
return out
|
|
66
|
+
if isinstance(node, list):
|
|
67
|
+
return [_drop_empty(x) for x in node]
|
|
68
|
+
return node
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# -- display helpers for ConfigMaps / Secrets ------------------------------
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def cap_map_values(obj: dict, limit: int = 2000) -> dict:
|
|
75
|
+
"""Cap long string values in data maps (ConfigMap display) in place."""
|
|
76
|
+
for key in ("data", "stringData", "binaryData"):
|
|
77
|
+
m = obj.get(key)
|
|
78
|
+
if not isinstance(m, dict):
|
|
79
|
+
continue
|
|
80
|
+
for k, v in list(m.items()):
|
|
81
|
+
if isinstance(v, str) and len(v) > limit:
|
|
82
|
+
m[k] = v[:limit] + f" ...[truncated {len(v) - limit} of {len(v)} chars; -v to see full]"
|
|
83
|
+
return obj
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def redact_map_values(obj: dict) -> dict:
|
|
87
|
+
"""Hide secret values in place; keys and sizes stay visible."""
|
|
88
|
+
for key in ("data", "stringData", "binaryData"):
|
|
89
|
+
m = obj.get(key)
|
|
90
|
+
if not isinstance(m, dict):
|
|
91
|
+
continue
|
|
92
|
+
obj[key] = {k: (f"<{len(v)} chars hidden -- use --reveal>" if isinstance(v, str) else v) for k, v in m.items()}
|
|
93
|
+
return obj
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def reveal_map_values(obj: dict) -> dict:
|
|
97
|
+
"""Base64-decode secret data values in place (binary values stay encoded)."""
|
|
98
|
+
m = obj.get("data")
|
|
99
|
+
if isinstance(m, dict):
|
|
100
|
+
out = {}
|
|
101
|
+
for k, v in m.items():
|
|
102
|
+
if not isinstance(v, str):
|
|
103
|
+
out[k] = v
|
|
104
|
+
continue
|
|
105
|
+
try:
|
|
106
|
+
raw = base64.b64decode(v)
|
|
107
|
+
except Exception:
|
|
108
|
+
out[k] = v
|
|
109
|
+
continue
|
|
110
|
+
try:
|
|
111
|
+
out[k] = raw.decode("utf-8")
|
|
112
|
+
except UnicodeDecodeError:
|
|
113
|
+
out[k] = f"<{len(raw)} bytes binary; base64: {v[:100]}>"
|
|
114
|
+
obj["data"] = out
|
|
115
|
+
return obj
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""kq — read-only Kubernetes queries for humans and AI agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
from .cli_kit import KqGroup
|
|
9
|
+
from .commands import register
|
|
10
|
+
from .config import Settings
|
|
11
|
+
from .output import FORMATS
|
|
12
|
+
|
|
13
|
+
EPILOG = """\
|
|
14
|
+
\b
|
|
15
|
+
USAGE PATTERNS
|
|
16
|
+
kq overview cluster snapshot: problems first
|
|
17
|
+
kq p l [-A|-n NS] [REGEX] list pods (d=deploy, n=ns, no=node,
|
|
18
|
+
kq p NAME / p desc NAME inspect sec=secret, cm, svc, ev)
|
|
19
|
+
kq logs NAME [-c C] [--tail N] pod logs kq tree deploy/NAME
|
|
20
|
+
kq get RESOURCE[/NAME] any resource incl. CRDs; kq api lists all
|
|
21
|
+
kq helm ls -A / kq eso -A helm releases / ExternalSecret status
|
|
22
|
+
|
|
23
|
+
\b
|
|
24
|
+
OPTIONS
|
|
25
|
+
-o yaml|json|rich|md|tree output (default: compact yaml)
|
|
26
|
+
-n NS | -A | -l SELECTOR | -f RX scope: namespace / all / labels / name regex
|
|
27
|
+
-v (system metadata) -vv (raw) verbosity
|
|
28
|
+
--ctx NAME / --kubeconfig PATH cluster selection
|
|
29
|
+
|
|
30
|
+
\b
|
|
31
|
+
ROLE
|
|
32
|
+
kq CAPTURES cluster state for diagnosis; fixing belongs to other tools
|
|
33
|
+
(kubectl apply, gitops, helm). kq only sends GET requests - it can
|
|
34
|
+
never create, update or delete anything.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@click.group(
|
|
39
|
+
cls=KqGroup,
|
|
40
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
41
|
+
)
|
|
42
|
+
@click.version_option(__version__, "--version", prog_name="kube-query")
|
|
43
|
+
@click.option("--ctx", "--context", "context", default=None, help="Kubeconfig context to use.")
|
|
44
|
+
@click.option("--kubeconfig", default=None, type=click.Path(exists=True), help="Path to a kubeconfig file.")
|
|
45
|
+
@click.option("-n", "--ns", "--namespace", "namespace", default=None, help="Default namespace for this invocation.")
|
|
46
|
+
@click.option(
|
|
47
|
+
"-A", "--all-namespaces", "all_namespaces", is_flag=True, default=False, help="All namespaces (where applicable)."
|
|
48
|
+
)
|
|
49
|
+
@click.option(
|
|
50
|
+
"-o",
|
|
51
|
+
"--output",
|
|
52
|
+
"output_fmt",
|
|
53
|
+
type=click.Choice(FORMATS),
|
|
54
|
+
default=None,
|
|
55
|
+
help="Output format: yaml, json, rich, md, tree. [env: KQ_OUTPUT]",
|
|
56
|
+
)
|
|
57
|
+
@click.option("-v", "--verbose", count=True, help="Verbosity: -v keep system metadata, -vv raw object.")
|
|
58
|
+
@click.pass_context
|
|
59
|
+
def main(ctx, context, kubeconfig, namespace, all_namespaces, output_fmt, verbose):
|
|
60
|
+
"""kq - read-only Kubernetes queries for AI agents and humans.
|
|
61
|
+
|
|
62
|
+
Compact yaml by default; rich tables, markdown, json and ownership
|
|
63
|
+
trees on request. Built-ins, shortnames, CRDs, Helm releases and
|
|
64
|
+
External Secrets Operator. Start with: kq overview
|
|
65
|
+
"""
|
|
66
|
+
ctx.obj = Settings(
|
|
67
|
+
context=context,
|
|
68
|
+
kubeconfig=kubeconfig,
|
|
69
|
+
namespace=namespace,
|
|
70
|
+
output=output_fmt,
|
|
71
|
+
verbose=verbose,
|
|
72
|
+
all_namespaces=all_namespaces,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
main.epilog = EPILOG
|
|
77
|
+
register(main)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
main()
|