|
58 | 58 | ) |
59 | 59 | from orb.providers.k8s.strategy.handler_registry import K8sHandlerRegistry |
60 | 60 | from orb.providers.k8s.value_objects import KubernetesProviderApi |
| 61 | +from orb.providers.k8s.watch.events_watcher import K8sEventsWatcher, K8sNodeEventsCache |
61 | 62 | from orb.providers.k8s.watch.multi_namespace import MultiNamespaceWatcher |
62 | 63 | from orb.providers.k8s.watch.node_state_cache import K8sNodeStateCache |
63 | 64 | from orb.providers.k8s.watch.node_watcher import K8sNodeWatcher |
@@ -157,6 +158,8 @@ def __init__( |
157 | 158 | orphan_gc: Optional[OrphanGarbageCollector] = None, |
158 | 159 | node_watcher: Optional[K8sNodeWatcher] = None, |
159 | 160 | node_state_cache: Optional[K8sNodeStateCache] = None, |
| 161 | + events_watcher: Optional[K8sEventsWatcher] = None, |
| 162 | + node_events_cache: Optional[K8sNodeEventsCache] = None, |
160 | 163 | native_spec_service: Optional[Any] = None, |
161 | 164 | ) -> None: |
162 | 165 | if not isinstance(config, K8sProviderConfig): |
@@ -191,6 +194,12 @@ def __init__( |
191 | 194 | # Tests inject both via the constructor kwargs to avoid real threads. |
192 | 195 | self._node_state_cache: K8sNodeStateCache = node_state_cache or K8sNodeStateCache() |
193 | 196 | self._node_watcher: Optional[K8sNodeWatcher] = node_watcher |
| 197 | + # Events API watching. When ``events_watch_enabled=True`` (opt-in via |
| 198 | + # K8sProviderConfig) the strategy starts a K8sEventsWatcher on a |
| 199 | + # background thread and populates K8sNodeEventsCache with Karpenter |
| 200 | + # node-disruption events. Tests inject both via constructor kwargs. |
| 201 | + self._node_events_cache: K8sNodeEventsCache = node_events_cache or K8sNodeEventsCache() |
| 202 | + self._events_watcher: Optional[K8sEventsWatcher] = events_watcher |
194 | 203 | # Native-spec escape hatch. Resolved lazily on first handler |
195 | 204 | # construction. ``None`` after resolution means the service is |
196 | 205 | # unavailable (jinja2 missing, injected service not provided, etc.) |
@@ -331,6 +340,7 @@ async def start_daemon_services(self) -> None: |
331 | 340 | self._maybe_start_watch_manager() |
332 | 341 | self._maybe_start_orphan_gc() |
333 | 342 | self._maybe_start_node_watcher() |
| 343 | + self._maybe_start_events_watcher() |
334 | 344 | self._daemon_services_started = True |
335 | 345 |
|
336 | 346 | def cleanup(self) -> None: |
@@ -368,6 +378,15 @@ def cleanup(self) -> None: |
368 | 378 | "Failed to stop Kubernetes node watcher during cleanup: %s", exc, exc_info=True |
369 | 379 | ) |
370 | 380 |
|
| 381 | + try: |
| 382 | + if self._events_watcher is not None: |
| 383 | + self._events_watcher.stop() |
| 384 | + self._events_watcher = None |
| 385 | + except Exception as exc: |
| 386 | + self._logger.warning( |
| 387 | + "Failed to stop Kubernetes events watcher during cleanup: %s", exc, exc_info=True |
| 388 | + ) |
| 389 | + |
371 | 390 | # Stage 4: client cleanup. Only clear ``_initialized`` when the |
372 | 391 | # client is successfully cleaned up — if this stage fails the |
373 | 392 | # provider can still serve reads via the existing connection while |
@@ -707,6 +726,48 @@ def _maybe_start_node_watcher(self) -> None: |
707 | 726 | except Exception as exc: |
708 | 727 | self._logger.warning("Failed to start Kubernetes node watcher: %s", exc, exc_info=True) |
709 | 728 |
|
| 729 | + # ------------------------------------------------------------------ |
| 730 | + # Events watcher lifecycle |
| 731 | + # ------------------------------------------------------------------ |
| 732 | + |
| 733 | + @property |
| 734 | + def node_events_cache(self) -> K8sNodeEventsCache: |
| 735 | + """The shared node-events cache populated by the Events API watcher. |
| 736 | +
|
| 737 | + Always present (never ``None``) -- when ``events_watch_enabled`` is |
| 738 | + ``False`` it is simply an empty cache that returns ``None`` for |
| 739 | + every lookup. |
| 740 | + """ |
| 741 | + return self._node_events_cache |
| 742 | + |
| 743 | + def _maybe_start_events_watcher(self) -> None: |
| 744 | + """Start the Events API watcher when enabled by config. |
| 745 | +
|
| 746 | + Like the node watcher, the events watcher runs on a plain |
| 747 | + background daemon thread (not in the asyncio event loop) so it |
| 748 | + can start from both synchronous (CLI bootstrap) and async |
| 749 | + (daemon) contexts. |
| 750 | +
|
| 751 | + Requires the operator to have granted the ``events: get/list/watch`` |
| 752 | + RBAC verb on the core API group -- see |
| 753 | + ``docs/root/providers/k8s/rbac.yaml``. |
| 754 | + """ |
| 755 | + if not self._k8s_config.events_watch_enabled: |
| 756 | + return |
| 757 | + if self._events_watcher is None: |
| 758 | + self._events_watcher = K8sEventsWatcher( |
| 759 | + kubernetes_client=self.kubernetes_client, |
| 760 | + cache=self._node_events_cache, |
| 761 | + logger=self._logger, |
| 762 | + ) |
| 763 | + try: |
| 764 | + self._events_watcher.start() |
| 765 | + self._logger.info("Kubernetes events watcher started (events_watch_enabled=True)") |
| 766 | + except Exception as exc: |
| 767 | + self._logger.warning( |
| 768 | + "Failed to start Kubernetes events watcher: %s", exc, exc_info=True |
| 769 | + ) |
| 770 | + |
710 | 771 | # ------------------------------------------------------------------ |
711 | 772 | # Operation dispatch |
712 | 773 | # ------------------------------------------------------------------ |
|
0 commit comments