k8stools 2.2.0__tar.gz → 2.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {k8stools-2.2.0 → k8stools-2.2.1}/CHANGELOG.md +17 -0
  2. {k8stools-2.2.0 → k8stools-2.2.1}/PKG-INFO +1 -1
  3. {k8stools-2.2.0 → k8stools-2.2.1}/pyproject.toml +1 -1
  4. {k8stools-2.2.0 → k8stools-2.2.1}/src/k8stools/__init__.py +1 -1
  5. {k8stools-2.2.0 → k8stools-2.2.1}/src/k8stools/k8s_tools.py +37 -1
  6. {k8stools-2.2.0 → k8stools-2.2.1}/tests/test_events.py +14 -0
  7. {k8stools-2.2.0 → k8stools-2.2.1}/uv.lock +1 -1
  8. {k8stools-2.2.0 → k8stools-2.2.1}/.claude/settings.local.json +0 -0
  9. {k8stools-2.2.0 → k8stools-2.2.1}/.github/workflows/unit-tests.yml +0 -0
  10. {k8stools-2.2.0 → k8stools-2.2.1}/.gitignore +0 -0
  11. {k8stools-2.2.0 → k8stools-2.2.1}/.python-version +0 -0
  12. {k8stools-2.2.0 → k8stools-2.2.1}/.vscode/mcp.json +0 -0
  13. {k8stools-2.2.0 → k8stools-2.2.1}/.vscode/settings.json +0 -0
  14. {k8stools-2.2.0 → k8stools-2.2.1}/CLAUDE.md +0 -0
  15. {k8stools-2.2.0 → k8stools-2.2.1}/LICENSE +0 -0
  16. {k8stools-2.2.0 → k8stools-2.2.1}/README.md +0 -0
  17. {k8stools-2.2.0 → k8stools-2.2.1}/chat-with-cluster.png +0 -0
  18. {k8stools-2.2.0 → k8stools-2.2.1}/designs/mock-state-capture.md +0 -0
  19. {k8stools-2.2.0 → k8stools-2.2.1}/docs/ROADMAP.md +0 -0
  20. {k8stools-2.2.0 → k8stools-2.2.1}/envrc.template +0 -0
  21. {k8stools-2.2.0 → k8stools-2.2.1}/src/k8stools/capture.py +0 -0
  22. {k8stools-2.2.0 → k8stools-2.2.1}/src/k8stools/fixtures/otel-demo.json +0 -0
  23. {k8stools-2.2.0 → k8stools-2.2.1}/src/k8stools/mcp_client.py +0 -0
  24. {k8stools-2.2.0 → k8stools-2.2.1}/src/k8stools/mcp_server.py +0 -0
  25. {k8stools-2.2.0 → k8stools-2.2.1}/src/k8stools/mock_state.py +0 -0
  26. {k8stools-2.2.0 → k8stools-2.2.1}/src/k8stools/mock_tools.py +0 -0
  27. {k8stools-2.2.0 → k8stools-2.2.1}/src/k8stools/redaction.py +0 -0
  28. {k8stools-2.2.0 → k8stools-2.2.1}/tests/conftest.py +0 -0
  29. {k8stools-2.2.0 → k8stools-2.2.1}/tests/test_capture.py +0 -0
  30. {k8stools-2.2.0 → k8stools-2.2.1}/tests/test_cluster_binding.py +0 -0
  31. {k8stools-2.2.0 → k8stools-2.2.1}/tests/test_duration_format.py +0 -0
  32. {k8stools-2.2.0 → k8stools-2.2.1}/tests/test_k8s_tools.py +0 -0
  33. {k8stools-2.2.0 → k8stools-2.2.1}/tests/test_k8s_tools_realk8s.py +0 -0
  34. {k8stools-2.2.0 → k8stools-2.2.1}/tests/test_log_decoding.py +0 -0
  35. {k8stools-2.2.0 → k8stools-2.2.1}/tests/test_mcp_client.py +0 -0
  36. {k8stools-2.2.0 → k8stools-2.2.1}/tests/test_mock_state.py +0 -0
  37. {k8stools-2.2.0 → k8stools-2.2.1}/tests/test_mock_tools.py +0 -0
  38. {k8stools-2.2.0 → k8stools-2.2.1}/tests/test_new_tools.py +0 -0
  39. {k8stools-2.2.0 → k8stools-2.2.1}/tests/test_redaction.py +0 -0
  40. {k8stools-2.2.0 → k8stools-2.2.1}/tests/test_replicaset_tool.py +0 -0
  41. {k8stools-2.2.0 → k8stools-2.2.1}/tests/test_version.py +0 -0
@@ -3,6 +3,23 @@
3
3
  All notable changes to k8stools. Versions follow [semantic versioning](https://semver.org/);
4
4
  dates are release-tag dates.
5
5
 
6
+ ## 2.2.1 — 2026-09-29
7
+
8
+ ### Documentation
9
+ - **Event records can lag behind what they count** (follow-up to issue #8). The
10
+ kubelet counts every occurrence but, by default, writes at most one update per
11
+ object and event type every 5 minutes once a burst of 25 is used up (client-go's
12
+ event spam filter). A crash-looping pod's `Created` record read `last_seen`
13
+ 29 minutes while the container had restarted 46 seconds earlier, then its count
14
+ jumped by six in one update. The `get_pod_events` and `get_events` descriptions
15
+ now say so: `count / (first_seen - last_seen)` still gives the rate, since both
16
+ lag together, but `last_seen` is not the time of the last restart (use the
17
+ container status or `PodSummary.last_restart`), and counts for different reasons
18
+ can differ by a few for the same restarts.
19
+ - `PodSummary.last_restart` is described as what it is: time since a container
20
+ last *terminated*. For a container waiting in CrashLoopBackOff, that is its last
21
+ crash, not a restart.
22
+
6
23
  ## 2.2.0 — 2026-09-28
7
24
 
8
25
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: k8stools
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: Tools and an MCP server for building Kubernetes agents
5
5
  Project-URL: Homepage, https://github.com/BenedatLLC/k8stools
6
6
  Project-URL: Changelog, https://github.com/BenedatLLC/k8stools/blob/main/CHANGELOG.md
@@ -1,7 +1,7 @@
1
1
 
2
2
  [project]
3
3
  name = "k8stools"
4
- version = "2.2.0"
4
+ version = "2.2.1"
5
5
  description = "Tools and an MCP server for building Kubernetes agents"
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.12"
@@ -78,4 +78,4 @@ the built-in OTel Demo snapshot by default, or your own via load_mock_state() or
78
78
  the server's --state-file option.
79
79
  """
80
80
 
81
- __version__ = "2.2.0"
81
+ __version__ = "2.2.1"
@@ -544,7 +544,11 @@ def get_pod_summaries(namespace: Optional[str] = None) -> list[PodSummary]:
544
544
  restarts : int
545
545
  Total number of restarts for all containers in the pod.
546
546
  last_restart : Optional[datetime.timedelta]
547
- Time since the container last restart (None if never restarted).
547
+ Time since a container of this pod last terminated - the most
548
+ recent last_state.finished_at across its containers - or None if
549
+ none has. For a container waiting in CrashLoopBackOff this is its
550
+ last crash, not a restart: it has not been started again yet. Taken
551
+ from the kubelet's status, so it is current, unlike event records.
548
552
  age : datetime.timedelta
549
553
  Age of the pod (current time minus creation timestamp).
550
554
  ip : Optional[str]
@@ -739,6 +743,22 @@ def get_pod_events(pod_name: str, namespace: str = "default") -> list[EventSumma
739
743
  kubelet waits to restart, so its count is several times the number
740
744
  of restarts. count divided by (first_seen - last_seen) is the
741
745
  average rate over that window, not the current back-off.
746
+
747
+ A record can lag behind what it counts. By default the kubelet
748
+ writes at most one event update per object and event type
749
+ ("Normal" or "Warning") every 5 minutes, once a burst of 25 is
750
+ used up; occurrences in between are counted but only written with
751
+ the next update, which then jumps by several at once. So count and
752
+ last_seen describe the most recent *written* occurrence and can
753
+ trail reality by several occurrences and tens of minutes. They lag
754
+ together, so the rate above still holds, but last_seen is not the
755
+ time of the last restart: for that, use the container status
756
+ (last_state.finished_at, state.started_at from
757
+ get_pod_container_statuses) or PodSummary.last_restart, which come
758
+ from the kubelet's status rather than from events. "Pulled",
759
+ "Created" and "Started" share one write budget, so their counts for
760
+ the same restarts can differ by a few; don't compare counts across
761
+ reasons.
742
762
  type : str
743
763
  Type of the event.
744
764
  reason : str
@@ -2507,6 +2527,22 @@ def get_events(namespace: Optional[str] = None,
2507
2527
  kubelet waits to restart, so its count is several times the number
2508
2528
  of restarts. count divided by (first_seen - last_seen) is the
2509
2529
  average rate over that window, not the current back-off.
2530
+
2531
+ A record can lag behind what it counts. By default the kubelet
2532
+ writes at most one event update per object and event type
2533
+ ("Normal" or "Warning") every 5 minutes, once a burst of 25 is
2534
+ used up; occurrences in between are counted but only written with
2535
+ the next update, which then jumps by several at once. So count and
2536
+ last_seen describe the most recent *written* occurrence and can
2537
+ trail reality by several occurrences and tens of minutes. They lag
2538
+ together, so the rate above still holds, but last_seen is not the
2539
+ time of the last restart: for that, use the container status
2540
+ (last_state.finished_at, state.started_at from
2541
+ get_pod_container_statuses) or PodSummary.last_restart, which come
2542
+ from the kubelet's status rather than from events. "Pulled",
2543
+ "Created" and "Started" share one write budget, so their counts for
2544
+ the same restarts can differ by a few; don't compare counts across
2545
+ reasons.
2510
2546
  type : str
2511
2547
  Type of the event ("Normal" or "Warning").
2512
2548
  reason : str
@@ -126,6 +126,20 @@ def test_docstrings_warn_that_backoff_counts_are_not_restarts():
126
126
  assert "first_seen" in tool.__doc__ and "count" in tool.__doc__
127
127
 
128
128
 
129
+ def test_docstrings_warn_that_records_lag_behind_what_they_count():
130
+ """client-go counts an occurrence before its spam filter decides whether to
131
+ write it, and the filter budgets writes per object and event type (burst 25,
132
+ then one per 5 minutes). On a real crash loop a Created record's last_seen
133
+ read 29 minutes while the container had restarted 46s earlier, then its
134
+ count jumped 595 -> 601. The docstring has to send agents to the container
135
+ status for the time of the last restart."""
136
+ for tool in (k8s_tools.get_pod_events, k8s_tools.get_events):
137
+ doc = tool.__doc__
138
+ assert "lag" in doc and "every 5 minutes" in doc
139
+ assert "last_state.finished_at" in doc and "PodSummary.last_restart" in doc
140
+ assert "don't compare counts across" in doc
141
+
142
+
129
143
  # --- capture and replay --------------------------------------------------------
130
144
 
131
145
  def _capture(events):
@@ -544,7 +544,7 @@ wheels = [
544
544
 
545
545
  [[package]]
546
546
  name = "k8stools"
547
- version = "2.2.0"
547
+ version = "2.2.1"
548
548
  source = { editable = "." }
549
549
  dependencies = [
550
550
  { name = "kubernetes" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes