Skip to content

feat: async k8s lib - #114

Merged
HardMax71 merged 3 commits into
mainfrom
feat/async-k8s-lib
Jan 28, 2026
Merged

feat: async k8s lib#114
HardMax71 merged 3 commits into
mainfrom
feat/async-k8s-lib

Conversation

@HardMax71

@HardMax71 HardMax71 commented Jan 27, 2026

Copy link
Copy Markdown
Owner

Summary by cubic

Migrated our Kubernetes integration to kubernetes_asyncio and refactored the worker, monitor, and resource cleaner to be fully async and DI-managed. This removes blocking calls, simplifies lifecycle handling, and improves reliability.

  • Refactors

    • Switched from kubernetes to kubernetes_asyncio (clients, watch, exceptions).
    • Removed K8sClients and LifecycleEnabled; services are provided via DI.
    • Updated KubernetesWorker, PodMonitor, and ResourceCleaner to async APIs and watchers; PodEventMapper supports async mapping.
    • Simplified run_k8s_worker bootstrap and added IdempotentConsumerWrapper.
    • Reworked tests to use kubernetes_asyncio models and shared factories; removed legacy pod stubs.
  • Dependencies

    • Replaced kubernetes==31.0.0 with kubernetes_asyncio==34.3.3 (lockfile updated).

Written for commit bb7f536. Summary will update on new commits.

Summary by CodeRabbit

  • Refactor
    • Migrated Kubernetes integration to an asynchronous client; simplified lifecycle and wiring for pod worker, monitor, and resource cleanup.
  • New Features
    • Worker now registers command handlers via dispatcher; scheduled background task ensures image pre-puller DaemonSet and improved graceful shutdown.
  • Tests
    • Converted tests and expanded fixtures to async-friendly mocks and utilities; updated e2e/unit tests to match async behavior.
  • Chores
    • Updated dependency to the async Kubernetes client.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Jan 27, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Removes the K8sClients bootstrap and migrates sync Kubernetes usage to kubernetes_asyncio.ApiClient injected via providers. Converts PodMonitor and PodEventMapper to async, refactors KubernetesWorker to dispatcher-driven async handlers, updates ResourceCleaner to use async API calls, and adapts DI, tests, and startup wiring accordingly.

Changes

Cohort / File(s) Summary
Kubernetes client module removed
backend/app/core/k8s_clients.py
Entire module deleted: K8sClients dataclass, create_k8s_clients(), and close_k8s_clients().
Provider DI & wiring
backend/app/core/providers.py
Replaced get_k8s_clients with get_api_client returning AsyncIterator[k8s_client.ApiClient]; providers updated to accept api_client; added dispatcher/consumer wiring (get_k8s_worker_dispatcher, get_kubernetes_worker, get_k8s_worker_consumer) and updated PodMonitor provider signatures.
Kubernetes worker refactor
backend/app/services/k8s_worker/worker.py
Switched to kubernetes_asyncio, constructor now receives api_client, producer, dispatcher; registers dispatcher handlers for CREATE/DELETE commands; replaced threaded sync calls with awaited async API calls; simplified lifecycle and removed legacy consumer/init flows.
Pod builder minor import
backend/app/services/k8s_worker/pod_builder.py
Switched import to kubernetes_asyncio (no functional change).
Pod event mapping → async
backend/app/services/pod_monitor/event_mapper.py
map_pod_event and internal mappers converted to async, added AsyncMapper alias, removed Protocol EventMapper, updated call sites to await async mappings and log extraction.
Pod monitor → async lifecycle
backend/app/services/pod_monitor/monitor.py
Replaced k8s_clients with api_client; PodMonitor now has explicit async start()/stop(), async watch loop using watch.stream, reconciliation via async API calls, and direct await publish paths.
Resource cleaner → async
backend/app/services/result_processor/resource_cleaner.py
Constructor now accepts api_client; instantiates CoreV1Api/NetworkingV1Api from it and converts listing/deletion flows to direct awaited async calls (removed executor usage).
Dependency update
backend/pyproject.toml
Replaced kubernetes==31.0.0 with kubernetes_asyncio==34.3.3.
E2E tests updated for async DI
backend/tests/e2e/test_k8s_worker_create_pod.py, backend/tests/e2e/test_resource_cleaner.py, backend/tests/e2e/test_admin_events_routes.py
Tests obtain ApiClient from DI, use EventDispatcher where applicable, await async Kubernetes API calls, and replace Kafka/schema/idempotency fixtures or polling with new patterns/fixtures.
Unit test fixtures & mocks added
backend/tests/unit/conftest.py
Added factories and fixtures: make_container_status, make_pod, MockWatchStream, make_mock_watch, make_mock_v1_api, and fixtures mock_pod, mock_v1_api, mock_watch.
Pod monitor unit conftest removed
backend/tests/unit/services/pod_monitor/conftest.py
Removed legacy per-module conftest (factories, mocks); functionality consolidated into root conftest and tests updated.
Pod monitor unit tests → async
backend/tests/unit/services/pod_monitor/test_event_mapper.py, backend/tests/unit/services/pod_monitor/test_monitor.py
Tests converted to async, use V1Pod and AsyncMock, added make_mock_api_client, updated make_pod_monitor to accept api_client, mock_v1, mock_watch, and reflect start/stop lifecycle.
Pod builder unit test import
backend/tests/unit/services/test_pod_builder.py
Switched to kubernetes_asyncio imports in tests.
Worker runtime entrypoint
backend/workers/run_k8s_worker.py
Initializes Beanie DB and event schemas, retrieves KubernetesWorker from DI, starts consumer via IdempotentConsumerWrapper, schedules image pre-puller DaemonSet task, and waits on a shutdown event for graceful exit.
Idempotency e2e tests rewritten
backend/tests/e2e/idempotency/test_consumer_idempotent.py
Reworked to test IdempotencyManager directly (no Kafka consumer wrapper); new async tests for reserve/complete/failed/remove behaviors.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Dispatcher as EventDispatcher
    participant Worker as KubernetesWorker
    participant K8sAPI as Kubernetes API
    participant Producer as KafkaProducer

    App->>Worker: __init__(api_client, producer, dispatcher, ...)
    Worker->>Dispatcher: register CREATE_POD_COMMAND handler
    Worker->>Dispatcher: register DELETE_POD_COMMAND handler

    App->>Dispatcher: dispatch(CREATE_POD_COMMAND)
    Dispatcher->>Worker: invoke create handler
    Worker->>K8sAPI: await create_namespaced_config_map()
    K8sAPI-->>Worker: configmap created
    Worker->>K8sAPI: await create_namespaced_pod()
    K8sAPI-->>Worker: pod created
    Worker->>Producer: produce(PodCreatedEvent)
    Producer-->>Worker: ack
Loading
sequenceDiagram
    participant App as Application
    participant Monitor as PodMonitor
    participant Watch as Kubernetes Watch
    participant Mapper as PodEventMapper
    participant K8sAPI as Kubernetes CoreV1 API
    participant Publisher as KafkaPublisher

    App->>Monitor: start()
    Monitor->>K8sAPI: init CoreV1Api (from ApiClient)
    Monitor->>Watch: start async watch stream
    loop on events
        Watch-->>Monitor: {type, object}
        Monitor->>Mapper: await map_pod_event(pod, type)
        Mapper->>K8sAPI: await read_namespaced_pod_log() if required
        Mapper-->>Monitor: DomainEvent
        Monitor->>Publisher: await publish(event)
    end
    App->>Monitor: stop()
    Monitor->>Watch: cancel/close
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped through code at break of dawn,
Old clients gone, new ApiClient drawn.
Dispatchers ring, async streams take flight,
Pods and watches hum all night.
A rabbit nods—refactor done, delight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: async k8s lib' clearly and concisely describes the primary change: migrating Kubernetes integration to use the asynchronous kubernetes_asyncio library instead of the synchronous kubernetes client.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/app/services/pod_monitor/event_mapper.py (1)

486-497: Missing exception handling for ast.literal_eval.

If ast.literal_eval fails to parse the text (e.g., malformed input), it will raise ValueError or SyntaxError, which will propagate up and cause _parse_executor_output to fail entirely. The method should catch these exceptions and return None.

Suggested fix
     def _try_parse_json(self, text: str) -> PodLogs | None:
         """Try to parse text as executor JSON output"""
         if not (text.startswith("{") and text.endswith("}")):
             return None
 
-        data = ast.literal_eval(text)
-        return PodLogs(
-            stdout=data.get("stdout", ""),
-            stderr=data.get("stderr", ""),
-            exit_code=data.get("exit_code", 0),
-            resource_usage=ResourceUsageDomain(**data.get("resource_usage", {})),
-        )
+        try:
+            data = ast.literal_eval(text)
+            return PodLogs(
+                stdout=data.get("stdout", ""),
+                stderr=data.get("stderr", ""),
+                exit_code=data.get("exit_code", 0),
+                resource_usage=ResourceUsageDomain(**data.get("resource_usage", {})),
+            )
+        except (ValueError, SyntaxError, TypeError, KeyError):
+            return None
🤖 Fix all issues with AI agents
In `@backend/app/core/providers.py`:
- Around line 790-796: The finally block is double-closing the
IdempotencyManager returned by MessagingProvider.get_idempotency_manager();
remove the explicit await idempotency_manager.close() call from this finally
(leaving await worker.wait_for_active_creations() and await
idempotent_consumer.stop() intact) so the manager's lifecycle stays solely
managed by get_idempotency_manager() and you avoid closing an already-closed
IdempotencyManager.
🧹 Nitpick comments (11)
backend/app/services/result_processor/resource_cleaner.py (1)

62-74: Consider removing the redundant read operation.

The read_namespaced_pod call (Line 65) checks if the pod exists before deletion, but delete_namespaced_pod already returns a 404 ApiException if the pod doesn't exist, which is handled in the except block. This adds unnecessary latency.

Suggested simplification
     async def _delete_pod(self, pod_name: str, namespace: str) -> None:
         """Delete a pod"""
         try:
-            await self.v1.read_namespaced_pod(pod_name, namespace)
             await self.v1.delete_namespaced_pod(pod_name, namespace, grace_period_seconds=30)
             self.logger.info(f"Deleted pod: {pod_name}")

         except ApiException as e:
             if e.status == 404:
                 self.logger.info(f"Pod {pod_name} already deleted")
             else:
                 self.logger.error(f"Failed to delete pod: {e}")
                 raise
backend/tests/unit/services/pod_monitor/test_event_mapper.py (1)

269-273: Weak assertion on cache behavior verification.

The assertions a == [] or all(x.event_type for x in a) only check that events have truthy event_type attributes, which is always true for valid events. Consider asserting specific expected behavior—for example, that duplicate events for the same pod/phase combination are filtered out.

Suggested improvement
     # Cache prevents duplicate for same phase
     p2 = make_pod(name="p2", phase="Running")
     a = await pem.map_pod_event(p2, "ADDED")
     b = await pem.map_pod_event(p2, "MODIFIED")
-    assert a == [] or all(x.event_type for x in a)
-    assert b == [] or all(x.event_type for x in b)
+    # First call should produce events, second call for same phase should be filtered
+    assert len(a) > 0, "First event should not be filtered"
+    assert len(b) == 0, "Duplicate phase event should be filtered by cache"
backend/pyproject.toml (1)

54-54: Use exact version pinning for consistency with other dependencies.

kubernetes_asyncio>=31.0.0 is the only dependency in this file using a version range (>=). All other 100+ dependencies use exact pinning with ==, which ensures reproducible builds. Changing this to ==31.0.0 would align with the project's dependency pinning strategy and prevent unexpected behavior from minor version updates.

Suggested fix
-    "kubernetes_asyncio>=31.0.0",
+    "kubernetes_asyncio==31.0.0",
backend/workers/run_k8s_worker.py (1)

40-42: Fire-and-forget task may lose exceptions silently.

The task created for ensure_image_pre_puller_daemonset() is not stored or awaited. If it raises an exception, it will be silently lost (only logged to stderr by asyncio). Consider storing the task reference and handling it during shutdown, or at minimum adding exception logging via task.add_done_callback().

Suggested improvement
+    # Bootstrap: ensure image pre-puller DaemonSet exists
+    prepuller_task = asyncio.create_task(worker.ensure_image_pre_puller_daemonset())
+    prepuller_task.add_done_callback(
+        lambda t: t.exception() and logger.error(f"Image pre-puller task failed: {t.exception()}")
+    )
-    # Bootstrap: ensure image pre-puller DaemonSet exists
-    asyncio.create_task(worker.ensure_image_pre_puller_daemonset())
     logger.info("Image pre-puller daemonset task scheduled")
backend/app/services/pod_monitor/monitor.py (1)

168-171: Redundant None check for _watch.

self._watch is always initialized in __init__ (line 114), so the if self._watch: check on line 169 is always true. This is not a bug, just unnecessary code.

backend/tests/e2e/test_k8s_worker_create_pod.py (1)

78-86: Test cleanup not guarded by try/finally.

If the assertions on lines 80-82 fail, the cleanup code on lines 85-86 won't execute, potentially leaving test resources (pods and configmaps) in the cluster. Consider wrapping the verification and cleanup in a try/finally block.

Suggested improvement
     # Verify resources exist
+    try:
         got_cm = await worker.v1.read_namespaced_config_map(name=f"script-{exec_id}", namespace=ns)
         assert got_cm is not None
         got_pod = await worker.v1.read_namespaced_pod(name=f"executor-{exec_id}", namespace=ns)
         assert got_pod is not None
-
-    # Cleanup
-    await worker.v1.delete_namespaced_pod(name=f"executor-{exec_id}", namespace=ns)
-    await worker.v1.delete_namespaced_config_map(name=f"script-{exec_id}", namespace=ns)
+    finally:
+        # Cleanup
+        try:
+            await worker.v1.delete_namespaced_pod(name=f"executor-{exec_id}", namespace=ns)
+        except ApiException:
+            pass
+        try:
+            await worker.v1.delete_namespaced_config_map(name=f"script-{exec_id}", namespace=ns)
+        except ApiException:
+            pass
backend/app/services/pod_monitor/event_mapper.py (1)

486-497: Consider using json.loads instead of ast.literal_eval for JSON parsing.

ast.literal_eval is designed for Python literals, not JSON. While it works for simple cases, JSON has different semantics (e.g., true/false vs True/False, null vs None). Since the executor output is documented as JSON, using json.loads would be more appropriate and semantically correct.

Suggested change
+import json
+
     def _try_parse_json(self, text: str) -> PodLogs | None:
         """Try to parse text as executor JSON output"""
         if not (text.startswith("{") and text.endswith("}")):
             return None
 
-        data = ast.literal_eval(text)
+        try:
+            data = json.loads(text)
+        except json.JSONDecodeError:
+            return None
         return PodLogs(
             stdout=data.get("stdout", ""),
             stderr=data.get("stderr", ""),
             exit_code=data.get("exit_code", 0),
             resource_usage=ResourceUsageDomain(**data.get("resource_usage", {})),
         )
backend/app/services/k8s_worker/worker.py (3)

84-94: Assertions for type checking can be disabled at runtime.

Using assert isinstance(event, ...) for type validation is fragile because assertions are disabled when Python runs with -O (optimize) flag. Consider raising a TypeError explicitly or using a type guard pattern for production code.

Suggested improvement
     async def _handle_create_pod_command_wrapper(self, event: DomainEvent) -> None:
         """Wrapper for handling CreatePodCommandEvent with type safety."""
-        assert isinstance(event, CreatePodCommandEvent)
+        if not isinstance(event, CreatePodCommandEvent):
+            raise TypeError(f"Expected CreatePodCommandEvent, got {type(event).__name__}")
         self.logger.info(f"Processing create_pod_command for execution {event.execution_id} from saga {event.saga_id}")
         await self._handle_create_pod_command(event)

     async def _handle_delete_pod_command_wrapper(self, event: DomainEvent) -> None:
         """Wrapper for handling DeletePodCommandEvent."""
-        assert isinstance(event, DeletePodCommandEvent)
+        if not isinstance(event, DeletePodCommandEvent):
+            raise TypeError(f"Expected DeletePodCommandEvent, got {type(event).__name__}")
         self.logger.info(f"Processing delete_pod_command for execution {event.execution_id} from saga {event.saga_id}")
         await self._handle_delete_pod_command(event)

104-106: Fire-and-forget task for pod creation loses exception visibility.

The task created by asyncio.create_task(self._create_pod_for_execution(command)) is not tracked beyond _active_creations. While the method has internal exception handling that publishes failure events, if an unexpected exception occurs before that (e.g., in the semaphore acquisition), it would be silently lost. Consider adding a done callback for error logging.


227-236: Consider aligning node_name handling with the pattern used in event_mapper.py.

The ExecutionStartedEvent class explicitly allows node_name to be None (typed as str | None = None), so passing pod.spec.node_name directly is valid. However, event_mapper.py (line 213) uses a fallback pattern with node_name=ctx.pod.spec.node_name or "pending" when publishing PodScheduledEvent. For consistency, consider whether this method should use a similar fallback (e.g., "pending") instead of allowing None, or document why the two approaches differ.

backend/tests/unit/services/pod_monitor/test_monitor.py (1)

8-9: Consider consolidating kubernetes_asyncio imports.

The kubernetes_asyncio imports are split across two separate locations (lines 8-9 and 31-33). Grouping them together would improve readability.

♻️ Suggested consolidation
-from kubernetes_asyncio import client as k8s_client
-
+from kubernetes_asyncio import client as k8s_client
+from kubernetes_asyncio.client import V1Pod
+from kubernetes_asyncio.client.rest import ApiException
 from app.core.metrics import EventMetrics, KubernetesMetrics
 ...
-from kubernetes_asyncio.client.rest import ApiException
-
-from kubernetes_asyncio.client import V1Pod

Also applies to: 31-33

Comment thread backend/app/core/providers.py

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 17 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="backend/pyproject.toml">

<violation number="1" location="backend/pyproject.toml:54">
P2: This dependency is no longer version-pinned while the rest of the project uses exact pins. That makes builds non-reproducible and can introduce untested upgrades. Pin kubernetes_asyncio to a specific version to match the project’s dependency strategy.</violation>
</file>

<file name="backend/app/core/providers.py">

<violation number="1" location="backend/app/core/providers.py:795">
P2: Avoid closing the shared IdempotencyManager inside the K8s worker consumer provider. Its lifecycle is already managed by the app-scoped provider, and closing it here can terminate shared state while other services still use it.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread backend/pyproject.toml Outdated
Comment thread backend/app/core/providers.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@backend/workers/run_k8s_worker.py`:
- Around line 40-43: The background task created with
asyncio.create_task(worker.ensure_image_pre_puller_daemonset())
(_daemonset_task) can swallow exceptions; attach a done-callback or spawn a
supervising coroutine that checks _daemonset_task.exception() and logs any
exception via logger.error (including traceback) so failures aren't lost. Locate
the create_task call that assigns _daemonset_task and add
_daemonset_task.add_done_callback(...) (or an async wrapper that awaits the task
and logs in an except block) referencing ensure_image_pre_puller_daemonset and
logger to surface errors.
🧹 Nitpick comments (2)
backend/app/core/providers.py (1)

719-743: Consider injecting K8sWorkerConfig instead of instantiating it twice.

K8sWorkerConfig() is instantiated here (line 734) and again in get_k8s_worker_consumer (line 757). This duplication could lead to inconsistency if config construction logic changes.

♻️ Suggested improvement

Add a provider method for K8sWorkerConfig and inject it:

+    `@provide`
+    def get_k8s_worker_config(self) -> K8sWorkerConfig:
+        return K8sWorkerConfig()
+
     `@provide`
     def get_kubernetes_worker(
         self,
         api_client: k8s_client.ApiClient,
         kafka_producer: UnifiedProducer,
         dispatcher: EventDispatcher,
+        config: K8sWorkerConfig,
         settings: Settings,
         logger: logging.Logger,
         event_metrics: EventMetrics,
     ) -> KubernetesWorker:
         """Create KubernetesWorker - registers handlers on dispatcher in constructor."""
-        config = K8sWorkerConfig()
         return KubernetesWorker(

Then inject config into get_k8s_worker_consumer as well.

backend/workers/run_k8s_worker.py (1)

36-38: Consumer retrieval relies on DI lifecycle - consider adding a clarifying comment.

The consumer is retrieved but not assigned because its lifecycle is managed by the DI container (provider starts it on creation, stops it on container.close()). This is correct but may confuse future maintainers.

📝 Suggested clarification
     # Get consumer (triggers consumer creation and start)
     # Consumer runs in background via its internal consume loop
-    await container.get(IdempotentConsumerWrapper)
+    # Note: Consumer lifecycle managed by DI container - stopped on container.close()
+    _ = await container.get(IdempotentConsumerWrapper)

Comment on lines +40 to +43
# Bootstrap: ensure image pre-puller DaemonSet exists
# Save task to variable to prevent premature garbage collection
_daemonset_task = asyncio.create_task(worker.ensure_image_pre_puller_daemonset())
logger.info("Image pre-puller daemonset task scheduled")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unobserved exceptions in background task.

If ensure_image_pre_puller_daemonset() raises an exception, it will be silently lost since the task result is never awaited or observed. Consider adding an exception callback to log failures.

🐛 Suggested fix
+    def _on_daemonset_task_done(task: asyncio.Task) -> None:
+        if task.cancelled():
+            return
+        if exc := task.exception():
+            logger.error(f"Image pre-puller daemonset task failed: {exc}")
+
     # Bootstrap: ensure image pre-puller DaemonSet exists
     # Save task to variable to prevent premature garbage collection
     _daemonset_task = asyncio.create_task(worker.ensure_image_pre_puller_daemonset())
+    _daemonset_task.add_done_callback(_on_daemonset_task_done)
     logger.info("Image pre-puller daemonset task scheduled")
🤖 Prompt for AI Agents
In `@backend/workers/run_k8s_worker.py` around lines 40 - 43, The background task
created with asyncio.create_task(worker.ensure_image_pre_puller_daemonset())
(_daemonset_task) can swallow exceptions; attach a done-callback or spawn a
supervising coroutine that checks _daemonset_task.exception() and logs any
exception via logger.error (including traceback) so failures aren't lost. Locate
the create_task call that assigns _daemonset_task and add
_daemonset_task.add_done_callback(...) (or an async wrapper that awaits the task
and logs in an except block) referencing ensure_image_pre_puller_daemonset and
logger to surface errors.

@sonarqubecloud

Copy link
Copy Markdown

@HardMax71
HardMax71 merged commit 9bd31a7 into main Jan 28, 2026
21 checks passed
@HardMax71
HardMax71 deleted the feat/async-k8s-lib branch January 28, 2026 10:44
@coderabbitai coderabbitai Bot mentioned this pull request Feb 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant