k8stools 2.0.4__tar.gz → 2.2.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.
- k8stools-2.2.0/CHANGELOG.md +210 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/CLAUDE.md +58 -2
- {k8stools-2.0.4 → k8stools-2.2.0}/PKG-INFO +90 -5
- {k8stools-2.0.4 → k8stools-2.2.0}/README.md +88 -4
- {k8stools-2.0.4 → k8stools-2.2.0}/designs/mock-state-capture.md +8 -3
- {k8stools-2.0.4 → k8stools-2.2.0}/docs/ROADMAP.md +5 -39
- {k8stools-2.0.4 → k8stools-2.2.0}/pyproject.toml +2 -2
- {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/__init__.py +1 -1
- {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/capture.py +24 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/k8s_tools.py +348 -64
- {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/mcp_client.py +3 -2
- {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/mcp_server.py +27 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/mock_state.py +113 -5
- {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/mock_tools.py +7 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_capture.py +139 -1
- k8stools-2.2.0/tests/test_cluster_binding.py +276 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_duration_format.py +1 -0
- k8stools-2.2.0/tests/test_events.py +164 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_k8s_tools.py +44 -1
- {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_k8s_tools_realk8s.py +58 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_mock_state.py +79 -2
- {k8stools-2.0.4 → k8stools-2.2.0}/uv.lock +1 -1
- k8stools-2.0.4/redaction-bug.md +0 -25
- k8stools-2.0.4/time-bug-report.md +0 -159
- {k8stools-2.0.4 → k8stools-2.2.0}/.claude/settings.local.json +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/.github/workflows/unit-tests.yml +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/.gitignore +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/.python-version +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/.vscode/mcp.json +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/.vscode/settings.json +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/LICENSE +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/chat-with-cluster.png +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/envrc.template +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/fixtures/otel-demo.json +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/src/k8stools/redaction.py +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/tests/conftest.py +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_log_decoding.py +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_mcp_client.py +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_mock_tools.py +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_new_tools.py +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_redaction.py +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_replicaset_tool.py +0 -0
- {k8stools-2.0.4 → k8stools-2.2.0}/tests/test_version.py +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to k8stools. Versions follow [semantic versioning](https://semver.org/);
|
|
4
|
+
dates are release-tag dates.
|
|
5
|
+
|
|
6
|
+
## 2.2.0 — 2026-09-28
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- **`count` and `first_seen` on events** (issue #8). `get_pod_events` and
|
|
10
|
+
`get_events` now report how many occurrences Kubernetes combined into each
|
|
11
|
+
event record, and how long ago the first one was, next to `last_seen`. For a
|
|
12
|
+
crash-looping container this gives the restart rate directly: on one cluster,
|
|
13
|
+
a `Created` count of 579 over 2 days 8 hours is a restart every ~5.8 minutes,
|
|
14
|
+
a number agents kept getting wrong from the other durations. The tool
|
|
15
|
+
descriptions explain the window the count covers and warn that `BackOff`
|
|
16
|
+
counts are several times the number of restarts. `print_pod_events` and
|
|
17
|
+
`print_events` gain a COUNT column.
|
|
18
|
+
- Captures store both fields; captures from before 2.2.0 replay them as `None`.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Replayed `captured_at` was on a different clock** (issue #7).
|
|
22
|
+
`get_cluster_info` returned the date in the capture file while every other
|
|
23
|
+
replayed timestamp was re-anchored to the server's start, so a pod killed
|
|
24
|
+
"5 minutes ago" appeared beside a capture taken a month ago. `captured_at` is
|
|
25
|
+
now the replay's anchor (the server's start time); the recorded date is in the
|
|
26
|
+
startup log and on `MockState.captured_at`.
|
|
27
|
+
- **Replayed logs kept their original timestamps.** Log line timestamps were not
|
|
28
|
+
re-anchored, so they contradicted every other replayed time, and
|
|
29
|
+
`since_seconds` (compared against the replay's "now") returned an empty log for
|
|
30
|
+
any capture older than the window, as if the container had logged nothing.
|
|
31
|
+
The kubelet's timestamp prefix on each line is now moved like every other
|
|
32
|
+
datetime, keeping its original precision and format. Timestamps an application
|
|
33
|
+
writes inside its messages are left as recorded.
|
|
34
|
+
- **Events recorded through `events.k8s.io/v1` had no time.** Their core/v1 view
|
|
35
|
+
leaves `lastTimestamp` empty and records the time in `eventTime` (and
|
|
36
|
+
repetition in `series`), so `last_seen` came back `None`. Both are now read
|
|
37
|
+
when the older fields are empty.
|
|
38
|
+
|
|
39
|
+
## 2.1.0 — 2026-09-28
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
- **Cluster selection.** `k8s-mcp-server` and `k8s-capture-state` take
|
|
43
|
+
`--kubeconfig PATH` and `--context NAME`; the context can also be pinned with
|
|
44
|
+
the `K8STOOLS_CONTEXT` environment variable. The order is flag, then
|
|
45
|
+
`KUBECONFIG` / `K8STOOLS_CONTEXT`, then `~/.kube/config` and its
|
|
46
|
+
`current-context`. The MCP server binds at startup and logs which cluster it is
|
|
47
|
+
answering from. Direct Python callers can call `k8s_tools.configure(kubeconfig,
|
|
48
|
+
context)`. See README, "Selecting a cluster".
|
|
49
|
+
- **`get_cluster_info` tool.** Reports which cluster the tools answer from: source
|
|
50
|
+
(`kubeconfig`, `in-cluster` or `capture`), context, API server URL, kubeconfig
|
|
51
|
+
path and server version. It never returns credentials. Replayed, it reports
|
|
52
|
+
`source="capture"` and when the capture was taken, so an agent can tell replayed
|
|
53
|
+
evidence from a live cluster.
|
|
54
|
+
- **Captures record their cluster**: the context, server URL and server version
|
|
55
|
+
are stored in a new optional `cluster` key, which `get_cluster_info` reads on
|
|
56
|
+
replay. The kubeconfig path is left out: it names a file on the capturing
|
|
57
|
+
machine. Older captures still load; older readers ignore the key.
|
|
58
|
+
- **`ContainerStateTerminated.ran_for`**: how long a terminated container instance
|
|
59
|
+
ran (`finished_at - started_at`), as a whole-second duration. It gives the
|
|
60
|
+
number an agent otherwise has to subtract, and often mixes up with how much
|
|
61
|
+
time the container's log covers or how often it restarts.
|
|
62
|
+
`get_pod_container_statuses` documents what it is not.
|
|
63
|
+
- `k8s_tools.Interval`, a duration type for a fixed span between two moments, next
|
|
64
|
+
to `Duration` (an age). Capture replay advances ages with the replay clock but
|
|
65
|
+
reads intervals back unchanged.
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
- **A cluster selection that cannot be loaded is an error.** A given
|
|
69
|
+
`--kubeconfig`, `--context` or `K8STOOLS_CONTEXT` that fails to load (missing
|
|
70
|
+
file, misspelled context) stops the MCP server and fails a capture, with the new
|
|
71
|
+
`K8sClusterSelectionError`. Previously every load failure fell back to in-cluster
|
|
72
|
+
config, which inside a pod meant silently binding to the pod's own cluster. With
|
|
73
|
+
no selection, the in-cluster fallback is unchanged, and the server still starts
|
|
74
|
+
when no configuration can be loaded.
|
|
75
|
+
- **All tools share one cluster binding, made once.** The core, apps and batch API
|
|
76
|
+
clients used to load the kubeconfig separately, each on first use, so a
|
|
77
|
+
`kubectl config use-context` between a server's first pod query and its first
|
|
78
|
+
deployment query left it answering from two clusters. A running server now
|
|
79
|
+
ignores later changes to the kubeconfig's current context.
|
|
80
|
+
- **k8stools no longer sets the kubernetes client's process-wide default
|
|
81
|
+
configuration.** Its binding uses a configuration of its own. Python code that
|
|
82
|
+
relied on k8stools having loaded the kubeconfig for its own kubernetes calls
|
|
83
|
+
must now load it itself (`kubernetes.config.load_kube_config()`).
|
|
84
|
+
- `--kubeconfig` and `--context` are rejected with `--mock` or `--state-file`,
|
|
85
|
+
which serve a capture rather than a cluster.
|
|
86
|
+
- `k8s-mcp-client` passes `K8STOOLS_CONTEXT` through to the server it starts, as
|
|
87
|
+
it already did `KUBECONFIG`.
|
|
88
|
+
|
|
89
|
+
## 2.0.4 — 2026-09-19
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
- **Pod logs arrived as a single unusable `b'...'` line** (issue #6). Since
|
|
93
|
+
kubernetes 36 the log endpoint's body reaches the client as `bytes`, and the
|
|
94
|
+
generated client turned it into `repr(bytes)`. Logs are now decoded where the
|
|
95
|
+
API is called, with invalid bytes replaced rather than failing the call, which
|
|
96
|
+
fixes the log tools, the Job/CronJob log readers and `k8s-capture-state` at once.
|
|
97
|
+
- Redaction handles `bytes` values explicitly.
|
|
98
|
+
|
|
99
|
+
### Documentation
|
|
100
|
+
- `previous=True` has three Kubernetes behaviors that look like bugs (identical
|
|
101
|
+
output during CrashLoopBackOff, a restart shifting the window between calls, and
|
|
102
|
+
a reclaimed log answering 200 with an error message). They are documented in the
|
|
103
|
+
tool's description and the README rather than changed.
|
|
104
|
+
|
|
105
|
+
## 2.0.3 — 2026-09-17
|
|
106
|
+
|
|
107
|
+
### Fixed
|
|
108
|
+
- **Duration fields failed strict schema validation.** Ages serialized with
|
|
109
|
+
fractional seconds (`"P2DT5M27.978616S"`), which the `format: "duration"` grammar
|
|
110
|
+
their own schema declares does not allow, so an MCP client that validates
|
|
111
|
+
structured output (ajv does) rejected every list tool on every row. Durations are
|
|
112
|
+
now whole, non-negative seconds (`k8s_tools.Duration`); a cluster clock ahead of
|
|
113
|
+
ours no longer yields a negative age.
|
|
114
|
+
- **Redaction blacked out whole values.** One secret-shaped substring replaced an
|
|
115
|
+
entire ConfigMap value, making config-as-JSON unreadable. Tokens (AWS keys, JWTs)
|
|
116
|
+
are now replaced where they sit. PEM private keys are still removed whole. A value
|
|
117
|
+
that parses as JSON is searched field by field, which also catches a
|
|
118
|
+
`{"password": ...}` inside it that was previously missed.
|
|
119
|
+
|
|
120
|
+
## 2.0.2 — 2026-09-12
|
|
121
|
+
|
|
122
|
+
Includes the 2.0.1 packaging fix, which was tagged but not published.
|
|
123
|
+
|
|
124
|
+
### Changed
|
|
125
|
+
- `k8s-mcp-client` always starts the server with the current interpreter
|
|
126
|
+
(`python -m k8stools.mcp_server`) instead of looking for `k8s-mcp-server` next
|
|
127
|
+
to `argv[0]`, and no longer prints a leftover debug line.
|
|
128
|
+
- `twine` and `build` dropped from the dev dependencies (uv replaces both).
|
|
129
|
+
|
|
130
|
+
### Fixed
|
|
131
|
+
- `rich` and `pydantic` are declared as runtime dependencies. `k8s-mcp-client`
|
|
132
|
+
imported `rich`, which was only a dev dependency, so it failed with
|
|
133
|
+
`ModuleNotFoundError` on any install from PyPI (2.0.0 and all earlier releases).
|
|
134
|
+
|
|
135
|
+
## 2.0.1 — 2026-09-12 (tagged, not published)
|
|
136
|
+
|
|
137
|
+
- The packaging fix listed under 2.0.2.
|
|
138
|
+
|
|
139
|
+
## 2.0.0 — 2026-09-12
|
|
140
|
+
|
|
141
|
+
### Added
|
|
142
|
+
- **Mock state capture and replay.** `k8s-capture-state` snapshots a live cluster
|
|
143
|
+
into one JSON file, and `k8s-mcp-server --state-file FILE` replays it through all
|
|
144
|
+
19 tools, so agents can be tested against realistic scenarios with no cluster.
|
|
145
|
+
`--state-time frozen` pins every age for repeatable test suites.
|
|
146
|
+
Previous-instance logs are captured, and captures can be written and read
|
|
147
|
+
gzipped. Design: `designs/mock-state-capture.md`.
|
|
148
|
+
|
|
149
|
+
### Changed (breaking)
|
|
150
|
+
- `mock_tools` serves one captured cluster (the built-in OTel Demo snapshot by
|
|
151
|
+
default): a pod not in the capture does not exist for any tool, instead of
|
|
152
|
+
getting a synthesized answer.
|
|
153
|
+
- Redaction's key-name rule matches whole words and exempts a field named exactly
|
|
154
|
+
`key`, which in the Kubernetes API is always structural. On a real 38-pod cluster
|
|
155
|
+
this went from 139 redactions to 5, still catching all 3 real secrets.
|
|
156
|
+
|
|
157
|
+
## 1.2.0 — 2026-09-11
|
|
158
|
+
|
|
159
|
+
### Added
|
|
160
|
+
- `get_replicaset_summaries`: a Deployment's replica sets, which are its change
|
|
161
|
+
history, sorted oldest revision first. Filtered to one deployment, the last entry
|
|
162
|
+
is its current revision, so "when did this last change, and what changed" is one
|
|
163
|
+
call.
|
|
164
|
+
|
|
165
|
+
## 1.1.0 — 2026-09-06
|
|
166
|
+
|
|
167
|
+
Delivered the Priority-1 batch from a heavy RCA user's feature request, plus the
|
|
168
|
+
cross-cutting redaction ask, and upgraded core dependencies.
|
|
169
|
+
|
|
170
|
+
### Added
|
|
171
|
+
- `get_configmap_summaries` / `get_configmap`: ConfigMap listing and full-content
|
|
172
|
+
read.
|
|
173
|
+
- `get_cronjob_summaries` / `get_job_summaries`: CronJob and Job spec and status,
|
|
174
|
+
including the pod template's container images and env; `owner` links a Job back
|
|
175
|
+
to its CronJob.
|
|
176
|
+
- `get_logs_for_job` / `get_logs_for_cronjob`: find the most recent pod of a Job,
|
|
177
|
+
or the most recent run of a CronJob, and return its logs.
|
|
178
|
+
- `get_pvc_summaries`: PVC listing with `mounted_by` pod resolution, which surfaces
|
|
179
|
+
orphaned PVCs.
|
|
180
|
+
- `get_events`: cluster- or namespace-wide events with server-side filtering
|
|
181
|
+
(`reason`, `involved_kind`, `involved_name`, `event_type`).
|
|
182
|
+
- `get_statefulset_summaries`: StatefulSet summaries mirroring
|
|
183
|
+
`DeploymentSummary`.
|
|
184
|
+
- `get_logs_for_pod_and_container`: `tail`, `since_seconds` and `previous`
|
|
185
|
+
(previous-instance logs, for crash-loop analysis).
|
|
186
|
+
- `get_node_summaries`: `capacity`, `allocatable`, `conditions`, `taints`, `labels`.
|
|
187
|
+
- `get_service_summaries`: `selector`, `labels`, `annotations`.
|
|
188
|
+
- **Secret redaction**: one pass at the MCP server's output boundary, on by
|
|
189
|
+
default, opt-out with `--no-redact` or `K8STOOLS_REDACT=0`. It matches by value
|
|
190
|
+
shape (AWS keys, JWTs, PEM private keys) and by key/env-var name
|
|
191
|
+
(`key|secret|token|password|credential`), replacing matches with a visible
|
|
192
|
+
`[REDACTED]` marker. Direct Python callers get raw values and can call
|
|
193
|
+
`redaction.redact_object` themselves.
|
|
194
|
+
|
|
195
|
+
### Changed
|
|
196
|
+
- `mcp` 1.12 → 2.1.1 (FastMCP → MCPServer), `kubernetes` 33.1 → 36.0.3.
|
|
197
|
+
|
|
198
|
+
## 1.0.1 — 2025-07-29
|
|
199
|
+
|
|
200
|
+
- README fixes.
|
|
201
|
+
|
|
202
|
+
## 1.0.0 — 2025-07-28
|
|
203
|
+
|
|
204
|
+
- First stable release: kubectl-style read-only tools for namespaces, nodes, pods,
|
|
205
|
+
container statuses, pod events, pod specs, logs, deployments and services, with
|
|
206
|
+
mock versions, as Python functions or through an MCP server.
|
|
207
|
+
|
|
208
|
+
## 0.1.0, 0.1.2 — 2025-07-21, 2025-07-23
|
|
209
|
+
|
|
210
|
+
- Initial releases (0.1.1 was tagged but not published).
|
|
@@ -24,11 +24,21 @@ tests/
|
|
|
24
24
|
test_capture.py - Capture serialization round-trip and capture-time redaction
|
|
25
25
|
test_k8s_tools_realk8s.py - Integration tests (real cluster, auto-skipped if unreachable)
|
|
26
26
|
test_mock_tools.py - Tests for mock_tools module (incl. parity with k8s_tools.TOOLS)
|
|
27
|
+
test_events.py - Event aggregation (count/first_seen), incl. the events.k8s.io fallback
|
|
28
|
+
test_cluster_binding.py - Cluster selection: configure(), the shared binding, get_cluster_info, server flags
|
|
27
29
|
test_log_decoding.py - Log decoding at the API boundary (bytes vs str), `previous` plumbing, log redaction
|
|
28
30
|
test_mcp_client.py - MCP client tests
|
|
29
31
|
test_version.py - Asserts pyproject and package __version__ agree
|
|
30
32
|
```
|
|
31
33
|
|
|
34
|
+
## Changelog
|
|
35
|
+
|
|
36
|
+
`CHANGELOG.md` records user-visible changes per release, newest first. Add to the
|
|
37
|
+
top entry (`## X.Y.Z — unreleased` until tagged) in the same change that makes
|
|
38
|
+
them, and call out behavior changes under **Changed**. It links from the README and
|
|
39
|
+
from the package metadata (`[project] urls`), so it shows on PyPI. `docs/ROADMAP.md`
|
|
40
|
+
holds only planned and deferred work.
|
|
41
|
+
|
|
32
42
|
## Environment setup
|
|
33
43
|
|
|
34
44
|
```bash
|
|
@@ -76,6 +86,12 @@ This is the same rule `MockState`'s replay clock already applies, for the same
|
|
|
76
86
|
reason. `tests/test_duration_format.py` checks each model's serialized output
|
|
77
87
|
against the format its own schema declares.
|
|
78
88
|
|
|
89
|
+
`Duration` means an *age* (time from some moment until now). For a fixed span
|
|
90
|
+
between two moments — `ContainerStateTerminated.ran_for` — use `k8s_tools.Interval`,
|
|
91
|
+
which has the same normalization and schema. The difference is capture replay:
|
|
92
|
+
`MockState` advances every age by the time since the server started, so a span
|
|
93
|
+
declared as `Duration` grows on replay (a 60s run read back as 1d+60s a day in).
|
|
94
|
+
|
|
79
95
|
Use snake_case field names. Include `pod_name`/`namespace` in container-level models for context.
|
|
80
96
|
|
|
81
97
|
All tools are collected in the `TOOLS` list in `k8s_tools.py` for agent/MCP registration.
|
|
@@ -109,7 +125,7 @@ raise K8sConfigError("Could not load kube config") # config/connection problem
|
|
|
109
125
|
raise K8sApiError(f"Error fetching pods: {e}") # API operation failures
|
|
110
126
|
```
|
|
111
127
|
|
|
112
|
-
##
|
|
128
|
+
## Cluster binding and the global API clients
|
|
113
129
|
|
|
114
130
|
Lazy singleton initialization pattern used throughout:
|
|
115
131
|
|
|
@@ -119,7 +135,33 @@ if K8S is None:
|
|
|
119
135
|
K8S = _get_api_client()
|
|
120
136
|
```
|
|
121
137
|
|
|
122
|
-
`_get_api_client()
|
|
138
|
+
`_get_api_client()`, `_get_apps_v1_api_client()` and `_get_batch_v1_api_client()`
|
|
139
|
+
all build on **one** `ApiClient`, the process's cluster binding (`_BINDING`),
|
|
140
|
+
resolved once by `configure(kubeconfig, context)` — called at startup by both CLIs —
|
|
141
|
+
or lazily on first use. Invariants:
|
|
142
|
+
|
|
143
|
+
- **One binding for every API group.** The helpers used to load the kubeconfig
|
|
144
|
+
separately on each group's first use, and each API object snapshots the global
|
|
145
|
+
default `Configuration` when constructed, so a `kubectl config use-context`
|
|
146
|
+
between a server's first pod query and its first deployment query split it
|
|
147
|
+
across two clusters. Don't reintroduce a per-helper `load_kube_config()`.
|
|
148
|
+
- **An explicit selection never falls back to in-cluster config.** The client
|
|
149
|
+
raises the same `ConfigException` for a misspelled context as for a missing
|
|
150
|
+
kubeconfig; falling back would bind a server in a pod to the pod's own cluster.
|
|
151
|
+
An explicit selection that fails raises `K8sClusterSelectionError` (a
|
|
152
|
+
`K8sConfigError`), which stops the MCP server; with no selection, the server
|
|
153
|
+
starts anyway and each call reports the problem, as it always has.
|
|
154
|
+
- **The binding never touches the process-wide default `Configuration`** (it passes
|
|
155
|
+
`client_configuration`), so library callers' own kubernetes clients are left alone.
|
|
156
|
+
- Resolution: `--kubeconfig` / argument, then `KUBECONFIG`, then `~/.kube/config`;
|
|
157
|
+
`--context` / argument, then `K8STOOLS_CONTEXT`, then `current-context`. There is
|
|
158
|
+
deliberately no `K8STOOLS_KUBECONFIG`: `KUBECONFIG` already is that variable.
|
|
159
|
+
The kubernetes client reads `KUBECONFIG` once, at import
|
|
160
|
+
(`KUBE_CONFIG_DEFAULT_LOCATION`), so setting it at runtime has no effect.
|
|
161
|
+
|
|
162
|
+
`get_cluster_info` reports the binding. Its replay counterpart answers from the
|
|
163
|
+
capture's optional top-level `cluster` record (context, server, server version —
|
|
164
|
+
never the kubeconfig path) with `source="capture"`.
|
|
123
165
|
|
|
124
166
|
## Using tools directly in an agent
|
|
125
167
|
|
|
@@ -145,6 +187,10 @@ k8s-mcp-server --mock
|
|
|
145
187
|
k8s-mcp-server --state-file incident-1234.json
|
|
146
188
|
k8s-mcp-server --state-file incident-1234.json --state-time frozen
|
|
147
189
|
|
|
190
|
+
# Pin a cluster (default: KUBECONFIG / ~/.kube/config and its current-context)
|
|
191
|
+
k8s-mcp-server --kubeconfig ~/.kube/prod.yaml --context prod-eu
|
|
192
|
+
K8STOOLS_CONTEXT=prod-eu k8s-mcp-server
|
|
193
|
+
|
|
148
194
|
# Disable secret redaction (on by default)
|
|
149
195
|
k8s-mcp-server --no-redact
|
|
150
196
|
```
|
|
@@ -168,6 +214,16 @@ Invariants worth preserving when touching this code:
|
|
|
168
214
|
replay — "deployment 8d old, current replica set 7h34m old, so it was upgraded
|
|
169
215
|
7h34m ago". The replay clock quantizes to whole seconds and samples once per
|
|
170
216
|
query to keep that exact; don't reintroduce per-field `datetime.now()`.
|
|
217
|
+
One clock for everything replay returns: datetimes are re-anchored so
|
|
218
|
+
capture time becomes server start time, and that includes
|
|
219
|
+
`get_cluster_info().captured_at` (the anchor itself) and the kubelet's
|
|
220
|
+
timestamp prefix on each log line (`_reanchor_log`). Anything that reports a
|
|
221
|
+
time from the file verbatim contradicts the rest. Tests that capture and
|
|
222
|
+
replay at the same instant cannot see this; use a capture date in the past.
|
|
223
|
+
Only ages advance: an `Interval` field is stored under the same `_seconds` key
|
|
224
|
+
but read back unchanged. Replay tests that freeze the clock can't tell the two
|
|
225
|
+
apart (frozen elapsed time is zero), so test a new duration field on an
|
|
226
|
+
advancing clock with a `server_start_time` in the past.
|
|
171
227
|
- **`MockState` must match the real tool's documented behavior**, not just its
|
|
172
228
|
types — filter semantics, result ordering (`get_replicaset_summaries` re-sorts
|
|
173
229
|
at query time), and which failures raise vs. return empty.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: k8stools
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Tools and an MCP server for building Kubernetes agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/BenedatLLC/k8stools
|
|
6
|
+
Project-URL: Changelog, https://github.com/BenedatLLC/k8stools/blob/main/CHANGELOG.md
|
|
6
7
|
Author: Jeff Fischer
|
|
7
8
|
License: Apache License
|
|
8
9
|
Version 2.0, January 2004
|
|
@@ -273,10 +274,14 @@ This installs three commands: `k8s-mcp-server` (the MCP server), `k8s-mcp-client
|
|
|
273
274
|
(a client for manual testing), and `k8s-capture-state` (snapshot a cluster to a
|
|
274
275
|
replayable file).
|
|
275
276
|
|
|
277
|
+
What changed in each release is in the
|
|
278
|
+
[changelog](https://github.com/BenedatLLC/k8stools/blob/main/CHANGELOG.md).
|
|
279
|
+
|
|
276
280
|
## Current tools
|
|
277
281
|
|
|
278
282
|
These are the tools we define:
|
|
279
283
|
|
|
284
|
+
* `get_cluster_info` - which cluster the tools are answering from: kubeconfig context, API server URL and version, or the capture being replayed
|
|
280
285
|
* `get_namespaces` - get a list of namespaces, like `kubectl get namespace`
|
|
281
286
|
* `get_node_summaries` - get a list of nodes, like `kubectl get nodes -o wide` (includes capacity/allocatable/conditions/taints/labels)
|
|
282
287
|
* `get_pod_summaries` - get a list of pods, like `kubectl get pods -o wide`
|
|
@@ -333,13 +338,25 @@ result = agent.run_sync("What is the status of the pods in my cluster?")
|
|
|
333
338
|
print(result.output)
|
|
334
339
|
```
|
|
335
340
|
|
|
341
|
+
The tools use `KUBECONFIG` (or `~/.kube/config`) and its current context. To pin a
|
|
342
|
+
cluster instead, call `configure` before the first tool call:
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
from k8stools import k8s_tools
|
|
346
|
+
|
|
347
|
+
k8s_tools.configure(kubeconfig="~/.kube/prod.yaml", context="prod-eu")
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
See [Selecting a cluster](#selecting-a-cluster) for how the selection is resolved.
|
|
351
|
+
|
|
336
352
|
### Using via MCP
|
|
337
353
|
The script `k8s-mcp-server` provides an MCP server for the same set of tools.
|
|
338
354
|
Here are the command line arguments for the server:
|
|
339
355
|
```
|
|
340
356
|
usage: k8s-mcp-server [-h] [--transport {streamable-http,stdio}] [--host HOST] [--port PORT]
|
|
341
|
-
[--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [--debug]
|
|
342
|
-
[--
|
|
357
|
+
[--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [--debug]
|
|
358
|
+
[--kubeconfig PATH] [--context NAME] [--mock] [--state-file FILE]
|
|
359
|
+
[--state-time {advancing,frozen}] [--no-redact]
|
|
343
360
|
|
|
344
361
|
Run the MCP server.
|
|
345
362
|
|
|
@@ -352,6 +369,11 @@ options:
|
|
|
352
369
|
--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
|
|
353
370
|
Log level [default: INFO]
|
|
354
371
|
--debug Enable debug mode [default: False]
|
|
372
|
+
--kubeconfig PATH Kubeconfig file to use [default: KUBECONFIG, then ~/.kube/config]
|
|
373
|
+
--context NAME Kubeconfig context to use [default: K8STOOLS_CONTEXT, then the
|
|
374
|
+
kubeconfig's current-context]. A kubeconfig or context that is given but
|
|
375
|
+
cannot be loaded stops the server rather than falling back to another
|
|
376
|
+
cluster.
|
|
355
377
|
--mock Run mock versions of the tools that don't need a cluster
|
|
356
378
|
--state-file FILE Serve a captured cluster snapshot from FILE (implies --mock)
|
|
357
379
|
--state-time {advancing,frozen}
|
|
@@ -359,6 +381,48 @@ options:
|
|
|
359
381
|
--no-redact Disable secret redaction of tool output (on by default)
|
|
360
382
|
```
|
|
361
383
|
|
|
384
|
+
#### Selecting a cluster
|
|
385
|
+
|
|
386
|
+
Each server answers from exactly one cluster, chosen at startup:
|
|
387
|
+
|
|
388
|
+
| | Kubeconfig file | Context |
|
|
389
|
+
|---|---|---|
|
|
390
|
+
| 1st | `--kubeconfig PATH` | `--context NAME` |
|
|
391
|
+
| 2nd | `KUBECONFIG` | `K8STOOLS_CONTEXT` |
|
|
392
|
+
| 3rd | `~/.kube/config` | the file's `current-context` |
|
|
393
|
+
|
|
394
|
+
With none of these given and no usable kubeconfig, the server falls back to the
|
|
395
|
+
in-cluster service account, as when it runs in a pod.
|
|
396
|
+
|
|
397
|
+
The server logs the binding when it starts:
|
|
398
|
+
|
|
399
|
+
```
|
|
400
|
+
WARNING:root:Bound to cluster: context 'prod-eu' at https://prod-eu.example:6443 (kubeconfig: /home/me/.kube/prod.yaml)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
and the `get_cluster_info` tool reports it to an agent, so an agent can check which
|
|
404
|
+
cluster it is talking to without leaving MCP.
|
|
405
|
+
|
|
406
|
+
A few rules keep a server from quietly answering from the wrong cluster:
|
|
407
|
+
|
|
408
|
+
* **An explicit selection never falls back.** If a `--kubeconfig`, `--context` or
|
|
409
|
+
`K8STOOLS_CONTEXT` is given but cannot be loaded (a missing file, a misspelled
|
|
410
|
+
context), the server exits with an error. It does not fall back to in-cluster
|
|
411
|
+
config, which in a pod would silently mean *that pod's* cluster.
|
|
412
|
+
* **The binding is made once.** Every tool shares it, and it does not follow later
|
|
413
|
+
changes to the kubeconfig: running `kubectl config use-context` does not
|
|
414
|
+
repoint a server that is already running.
|
|
415
|
+
* **`--kubeconfig` and `--context` are rejected with `--mock` or `--state-file`**,
|
|
416
|
+
which serve a capture rather than a cluster.
|
|
417
|
+
|
|
418
|
+
To serve two clusters, run two servers:
|
|
419
|
+
|
|
420
|
+
```sh
|
|
421
|
+
k8s-mcp-server --transport=streamable-http --port=8001 --context prod-eu
|
|
422
|
+
k8s-mcp-server --transport=streamable-http --port=8002 \
|
|
423
|
+
--kubeconfig ~/.kube/eks.yaml --context staging
|
|
424
|
+
```
|
|
425
|
+
|
|
362
426
|
#### Use MCP with the stdio transport
|
|
363
427
|
The *stdio* transport is best for use with local Coding Agents, like GitHub CoPilot or Cursor.
|
|
364
428
|
It is the default, so you can run the `k8s-mcp-server` script without arguments. Here's an example
|
|
@@ -381,7 +445,8 @@ This assumes the following:
|
|
|
381
445
|
1. The Python virtual environment is expected to be in `.venv` under the root of your VSCode workspace
|
|
382
446
|
2. You have installed the k8stools package into your workspace
|
|
383
447
|
3. The environment file `.envrc` contains any variables you need defined. In particular, you may need to
|
|
384
|
-
set `KUBECONFIG` to point to your `kubectl` config file
|
|
448
|
+
set `KUBECONFIG` to point to your `kubectl` config file, and `K8STOOLS_CONTEXT` to pin a context
|
|
449
|
+
(or pass `--kubeconfig` / `--context` in `args`).
|
|
385
450
|
|
|
386
451
|
#### Use MCP with the streamable HTTP transport
|
|
387
452
|
The *streamable http* transport is enabled with the command line option `--transport=streamable-http`. It will
|
|
@@ -466,7 +531,8 @@ k8s-mcp-server --state-file incident-1234.json
|
|
|
466
531
|
### `k8s-capture-state` options
|
|
467
532
|
|
|
468
533
|
```
|
|
469
|
-
usage: k8s-capture-state [-h] [--namespace NS [NS ...]] [-o FILE]
|
|
534
|
+
usage: k8s-capture-state [-h] [--namespace NS [NS ...]] [-o FILE]
|
|
535
|
+
[--kubeconfig PATH] [--context NAME] [--no-logs]
|
|
470
536
|
[--max-log-lines MAX_LOG_LINES] [--no-previous-logs]
|
|
471
537
|
[--no-redact]
|
|
472
538
|
[--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
|
|
@@ -479,6 +545,11 @@ options:
|
|
|
479
545
|
captured in full.
|
|
480
546
|
-o FILE, --output FILE
|
|
481
547
|
Output file [default: k8s-state-<timestamp>.json]
|
|
548
|
+
--kubeconfig PATH Kubeconfig file to capture from [default: KUBECONFIG,
|
|
549
|
+
then ~/.kube/config]
|
|
550
|
+
--context NAME Kubeconfig context to capture from [default:
|
|
551
|
+
K8STOOLS_CONTEXT, then the kubeconfig's current-
|
|
552
|
+
context]
|
|
482
553
|
--no-logs Skip container logs
|
|
483
554
|
--max-log-lines MAX_LOG_LINES
|
|
484
555
|
Log lines to capture per container [default: 1000]
|
|
@@ -507,11 +578,16 @@ Everything the tools can read, so that every tool answers on replay:
|
|
|
507
578
|
|
|
508
579
|
| | |
|
|
509
580
|
|---|---|
|
|
581
|
+
| Cluster | context name, API server URL and version, so `get_cluster_info` answers on replay (not the kubeconfig path, which names a file on the capturing machine) |
|
|
510
582
|
| Cluster-wide | namespaces, nodes |
|
|
511
583
|
| Per namespace | deployments, replica sets, services, statefulsets, cronjobs, jobs, PVCs, events |
|
|
512
584
|
| ConfigMaps | summary **and** full contents, so `get_configmap` works too |
|
|
513
585
|
| Per pod | summary, labels, container statuses, spec, and per-container logs |
|
|
514
586
|
|
|
587
|
+
A cluster is selected exactly as for the MCP server (see [Selecting a
|
|
588
|
+
cluster](#selecting-a-cluster)), and a selection that cannot be loaded fails the
|
|
589
|
+
capture.
|
|
590
|
+
|
|
515
591
|
`--namespace` restricts only the namespaced resources; namespaces and nodes are
|
|
516
592
|
always captured in full so the cluster still makes sense.
|
|
517
593
|
|
|
@@ -608,6 +684,15 @@ long session cannot see ages drift between its first tool call and its last.
|
|
|
608
684
|
a suite that believed it had frozen the clock and had not would be flaky with no
|
|
609
685
|
visible cause.)
|
|
610
686
|
|
|
687
|
+
Absolute times are replayed the same way: every timestamp a tool returns is moved
|
|
688
|
+
so the moment of capture becomes the moment the server started. A container killed
|
|
689
|
+
5 minutes before the capture reads as killed 5 minutes before server start, and
|
|
690
|
+
`get_cluster_info`'s `captured_at` is that start time, not the date in the file,
|
|
691
|
+
so the agent sees one consistent clock. Log line timestamps (the kubelet's
|
|
692
|
+
prefix) are moved too, so `since_seconds` works on a capture of any age;
|
|
693
|
+
timestamps an application writes inside its own log messages are left as
|
|
694
|
+
recorded. The file's own capture date is in the server's startup log.
|
|
695
|
+
|
|
611
696
|
### Replaying from Python
|
|
612
697
|
|
|
613
698
|
For a test suite, skip the server and load the capture directly. `MockState` has
|
|
@@ -54,10 +54,14 @@ This installs three commands: `k8s-mcp-server` (the MCP server), `k8s-mcp-client
|
|
|
54
54
|
(a client for manual testing), and `k8s-capture-state` (snapshot a cluster to a
|
|
55
55
|
replayable file).
|
|
56
56
|
|
|
57
|
+
What changed in each release is in the
|
|
58
|
+
[changelog](https://github.com/BenedatLLC/k8stools/blob/main/CHANGELOG.md).
|
|
59
|
+
|
|
57
60
|
## Current tools
|
|
58
61
|
|
|
59
62
|
These are the tools we define:
|
|
60
63
|
|
|
64
|
+
* `get_cluster_info` - which cluster the tools are answering from: kubeconfig context, API server URL and version, or the capture being replayed
|
|
61
65
|
* `get_namespaces` - get a list of namespaces, like `kubectl get namespace`
|
|
62
66
|
* `get_node_summaries` - get a list of nodes, like `kubectl get nodes -o wide` (includes capacity/allocatable/conditions/taints/labels)
|
|
63
67
|
* `get_pod_summaries` - get a list of pods, like `kubectl get pods -o wide`
|
|
@@ -114,13 +118,25 @@ result = agent.run_sync("What is the status of the pods in my cluster?")
|
|
|
114
118
|
print(result.output)
|
|
115
119
|
```
|
|
116
120
|
|
|
121
|
+
The tools use `KUBECONFIG` (or `~/.kube/config`) and its current context. To pin a
|
|
122
|
+
cluster instead, call `configure` before the first tool call:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from k8stools import k8s_tools
|
|
126
|
+
|
|
127
|
+
k8s_tools.configure(kubeconfig="~/.kube/prod.yaml", context="prod-eu")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
See [Selecting a cluster](#selecting-a-cluster) for how the selection is resolved.
|
|
131
|
+
|
|
117
132
|
### Using via MCP
|
|
118
133
|
The script `k8s-mcp-server` provides an MCP server for the same set of tools.
|
|
119
134
|
Here are the command line arguments for the server:
|
|
120
135
|
```
|
|
121
136
|
usage: k8s-mcp-server [-h] [--transport {streamable-http,stdio}] [--host HOST] [--port PORT]
|
|
122
|
-
[--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [--debug]
|
|
123
|
-
[--
|
|
137
|
+
[--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [--debug]
|
|
138
|
+
[--kubeconfig PATH] [--context NAME] [--mock] [--state-file FILE]
|
|
139
|
+
[--state-time {advancing,frozen}] [--no-redact]
|
|
124
140
|
|
|
125
141
|
Run the MCP server.
|
|
126
142
|
|
|
@@ -133,6 +149,11 @@ options:
|
|
|
133
149
|
--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
|
|
134
150
|
Log level [default: INFO]
|
|
135
151
|
--debug Enable debug mode [default: False]
|
|
152
|
+
--kubeconfig PATH Kubeconfig file to use [default: KUBECONFIG, then ~/.kube/config]
|
|
153
|
+
--context NAME Kubeconfig context to use [default: K8STOOLS_CONTEXT, then the
|
|
154
|
+
kubeconfig's current-context]. A kubeconfig or context that is given but
|
|
155
|
+
cannot be loaded stops the server rather than falling back to another
|
|
156
|
+
cluster.
|
|
136
157
|
--mock Run mock versions of the tools that don't need a cluster
|
|
137
158
|
--state-file FILE Serve a captured cluster snapshot from FILE (implies --mock)
|
|
138
159
|
--state-time {advancing,frozen}
|
|
@@ -140,6 +161,48 @@ options:
|
|
|
140
161
|
--no-redact Disable secret redaction of tool output (on by default)
|
|
141
162
|
```
|
|
142
163
|
|
|
164
|
+
#### Selecting a cluster
|
|
165
|
+
|
|
166
|
+
Each server answers from exactly one cluster, chosen at startup:
|
|
167
|
+
|
|
168
|
+
| | Kubeconfig file | Context |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| 1st | `--kubeconfig PATH` | `--context NAME` |
|
|
171
|
+
| 2nd | `KUBECONFIG` | `K8STOOLS_CONTEXT` |
|
|
172
|
+
| 3rd | `~/.kube/config` | the file's `current-context` |
|
|
173
|
+
|
|
174
|
+
With none of these given and no usable kubeconfig, the server falls back to the
|
|
175
|
+
in-cluster service account, as when it runs in a pod.
|
|
176
|
+
|
|
177
|
+
The server logs the binding when it starts:
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
WARNING:root:Bound to cluster: context 'prod-eu' at https://prod-eu.example:6443 (kubeconfig: /home/me/.kube/prod.yaml)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
and the `get_cluster_info` tool reports it to an agent, so an agent can check which
|
|
184
|
+
cluster it is talking to without leaving MCP.
|
|
185
|
+
|
|
186
|
+
A few rules keep a server from quietly answering from the wrong cluster:
|
|
187
|
+
|
|
188
|
+
* **An explicit selection never falls back.** If a `--kubeconfig`, `--context` or
|
|
189
|
+
`K8STOOLS_CONTEXT` is given but cannot be loaded (a missing file, a misspelled
|
|
190
|
+
context), the server exits with an error. It does not fall back to in-cluster
|
|
191
|
+
config, which in a pod would silently mean *that pod's* cluster.
|
|
192
|
+
* **The binding is made once.** Every tool shares it, and it does not follow later
|
|
193
|
+
changes to the kubeconfig: running `kubectl config use-context` does not
|
|
194
|
+
repoint a server that is already running.
|
|
195
|
+
* **`--kubeconfig` and `--context` are rejected with `--mock` or `--state-file`**,
|
|
196
|
+
which serve a capture rather than a cluster.
|
|
197
|
+
|
|
198
|
+
To serve two clusters, run two servers:
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
k8s-mcp-server --transport=streamable-http --port=8001 --context prod-eu
|
|
202
|
+
k8s-mcp-server --transport=streamable-http --port=8002 \
|
|
203
|
+
--kubeconfig ~/.kube/eks.yaml --context staging
|
|
204
|
+
```
|
|
205
|
+
|
|
143
206
|
#### Use MCP with the stdio transport
|
|
144
207
|
The *stdio* transport is best for use with local Coding Agents, like GitHub CoPilot or Cursor.
|
|
145
208
|
It is the default, so you can run the `k8s-mcp-server` script without arguments. Here's an example
|
|
@@ -162,7 +225,8 @@ This assumes the following:
|
|
|
162
225
|
1. The Python virtual environment is expected to be in `.venv` under the root of your VSCode workspace
|
|
163
226
|
2. You have installed the k8stools package into your workspace
|
|
164
227
|
3. The environment file `.envrc` contains any variables you need defined. In particular, you may need to
|
|
165
|
-
set `KUBECONFIG` to point to your `kubectl` config file
|
|
228
|
+
set `KUBECONFIG` to point to your `kubectl` config file, and `K8STOOLS_CONTEXT` to pin a context
|
|
229
|
+
(or pass `--kubeconfig` / `--context` in `args`).
|
|
166
230
|
|
|
167
231
|
#### Use MCP with the streamable HTTP transport
|
|
168
232
|
The *streamable http* transport is enabled with the command line option `--transport=streamable-http`. It will
|
|
@@ -247,7 +311,8 @@ k8s-mcp-server --state-file incident-1234.json
|
|
|
247
311
|
### `k8s-capture-state` options
|
|
248
312
|
|
|
249
313
|
```
|
|
250
|
-
usage: k8s-capture-state [-h] [--namespace NS [NS ...]] [-o FILE]
|
|
314
|
+
usage: k8s-capture-state [-h] [--namespace NS [NS ...]] [-o FILE]
|
|
315
|
+
[--kubeconfig PATH] [--context NAME] [--no-logs]
|
|
251
316
|
[--max-log-lines MAX_LOG_LINES] [--no-previous-logs]
|
|
252
317
|
[--no-redact]
|
|
253
318
|
[--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
|
|
@@ -260,6 +325,11 @@ options:
|
|
|
260
325
|
captured in full.
|
|
261
326
|
-o FILE, --output FILE
|
|
262
327
|
Output file [default: k8s-state-<timestamp>.json]
|
|
328
|
+
--kubeconfig PATH Kubeconfig file to capture from [default: KUBECONFIG,
|
|
329
|
+
then ~/.kube/config]
|
|
330
|
+
--context NAME Kubeconfig context to capture from [default:
|
|
331
|
+
K8STOOLS_CONTEXT, then the kubeconfig's current-
|
|
332
|
+
context]
|
|
263
333
|
--no-logs Skip container logs
|
|
264
334
|
--max-log-lines MAX_LOG_LINES
|
|
265
335
|
Log lines to capture per container [default: 1000]
|
|
@@ -288,11 +358,16 @@ Everything the tools can read, so that every tool answers on replay:
|
|
|
288
358
|
|
|
289
359
|
| | |
|
|
290
360
|
|---|---|
|
|
361
|
+
| Cluster | context name, API server URL and version, so `get_cluster_info` answers on replay (not the kubeconfig path, which names a file on the capturing machine) |
|
|
291
362
|
| Cluster-wide | namespaces, nodes |
|
|
292
363
|
| Per namespace | deployments, replica sets, services, statefulsets, cronjobs, jobs, PVCs, events |
|
|
293
364
|
| ConfigMaps | summary **and** full contents, so `get_configmap` works too |
|
|
294
365
|
| Per pod | summary, labels, container statuses, spec, and per-container logs |
|
|
295
366
|
|
|
367
|
+
A cluster is selected exactly as for the MCP server (see [Selecting a
|
|
368
|
+
cluster](#selecting-a-cluster)), and a selection that cannot be loaded fails the
|
|
369
|
+
capture.
|
|
370
|
+
|
|
296
371
|
`--namespace` restricts only the namespaced resources; namespaces and nodes are
|
|
297
372
|
always captured in full so the cluster still makes sense.
|
|
298
373
|
|
|
@@ -389,6 +464,15 @@ long session cannot see ages drift between its first tool call and its last.
|
|
|
389
464
|
a suite that believed it had frozen the clock and had not would be flaky with no
|
|
390
465
|
visible cause.)
|
|
391
466
|
|
|
467
|
+
Absolute times are replayed the same way: every timestamp a tool returns is moved
|
|
468
|
+
so the moment of capture becomes the moment the server started. A container killed
|
|
469
|
+
5 minutes before the capture reads as killed 5 minutes before server start, and
|
|
470
|
+
`get_cluster_info`'s `captured_at` is that start time, not the date in the file,
|
|
471
|
+
so the agent sees one consistent clock. Log line timestamps (the kubelet's
|
|
472
|
+
prefix) are moved too, so `since_seconds` works on a capture of any age;
|
|
473
|
+
timestamps an application writes inside its own log messages are left as
|
|
474
|
+
recorded. The file's own capture date is in the server's startup log.
|
|
475
|
+
|
|
392
476
|
### Replaying from Python
|
|
393
477
|
|
|
394
478
|
For a test suite, skip the server and load the capture directly. `MockState` has
|