From 9b1af145f42a153f464a3f0ce54c334c3ba51e7a Mon Sep 17 00:00:00 2001 From: suger Date: Tue, 10 Mar 2026 21:36:45 +0800 Subject: [PATCH] fix(server): include manual k8s sandboxes in list API --- server/src/services/k8s/kubernetes_service.py | 9 ++- server/tests/k8s/test_kubernetes_service.py | 55 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/server/src/services/k8s/kubernetes_service.py b/server/src/services/k8s/kubernetes_service.py index 772a409b..114794a0 100644 --- a/server/src/services/k8s/kubernetes_service.py +++ b/server/src/services/k8s/kubernetes_service.py @@ -427,8 +427,9 @@ def list_sandboxes(self, request: ListSandboxesRequest) -> ListSandboxesResponse ListSandboxesResponse: Paginated list of sandboxes """ try: - # Build label selector - label_selector = SANDBOX_ID_LABEL + # Do not require opensandbox.io/id when listing. + # This allows manually created CRs (e.g. BatchSandbox) to appear. + label_selector = "" # List all workloads workloads = self.workload_provider.list_workloads( @@ -695,13 +696,15 @@ def _build_sandbox_from_workload(self, workload: Any) -> Sandbox: spec = workload.get("spec", {}) labels = metadata.get("labels", {}) creation_timestamp = metadata.get("creationTimestamp") + resource_name = metadata.get("name", "") else: metadata = workload.metadata spec = workload.spec labels = metadata.labels or {} creation_timestamp = metadata.creation_timestamp + resource_name = getattr(metadata, "name", "") - sandbox_id = labels.get(SANDBOX_ID_LABEL, "") + sandbox_id = labels.get(SANDBOX_ID_LABEL) or resource_name # Get expiration from provider expires_at = self.workload_provider.get_expiration(workload) diff --git a/server/tests/k8s/test_kubernetes_service.py b/server/tests/k8s/test_kubernetes_service.py index 5fc1be6b..5a5937e3 100644 --- a/server/tests/k8s/test_kubernetes_service.py +++ b/server/tests/k8s/test_kubernetes_service.py @@ -378,6 +378,61 @@ def test_list_all_sandboxes_succeeds(self, k8s_service, mock_workload): assert len(response.items) == 1 assert response.items[0].id == "test-sandbox-123" assert response.pagination.total_items == 1 + + def test_list_sandboxes_includes_manual_workload_without_id_label(self, k8s_service, mock_workload): + """ + Test case: Workloads without opensandbox.io/id label are still listed. + + Purpose: Ensure manually created BatchSandbox resources are visible in list API + and sandbox id falls back to metadata.name. + """ + manual_workload = { + "metadata": { + "name": "manual-batchsandbox", + "uid": "manual-uid", + "labels": { + "team": "manual", + }, + "annotations": mock_workload["metadata"]["annotations"].copy(), + "creationTimestamp": datetime.now(timezone.utc).isoformat(), + }, + "spec": { + "template": { + "spec": { + "containers": [ + { + "name": "sandbox", + "image": "python:3.11", + "command": ["/bin/sh", "-c", "sleep 3600"], + } + ] + } + } + }, + "status": {}, + } + k8s_service.workload_provider.list_workloads.return_value = [manual_workload] + k8s_service.workload_provider.get_status.return_value = { + "state": "Running", + "reason": "", + "message": "Running", + "last_transition_at": datetime.now(timezone.utc), + } + k8s_service.workload_provider.get_expiration.return_value = ( + datetime.now(timezone.utc) + timedelta(hours=1) + ) + + from src.api.schema import PaginationRequest + request = ListSandboxesRequest(pagination=PaginationRequest(page=1, page_size=20)) + response = k8s_service.list_sandboxes(request) + + assert len(response.items) == 1 + assert response.items[0].id == "manual-batchsandbox" + assert response.items[0].metadata == {"team": "manual"} + k8s_service.workload_provider.list_workloads.assert_called_once_with( + namespace=k8s_service.namespace, + label_selector="", + ) def test_list_sandboxes_with_pagination(self, k8s_service, mock_workload): """