Files
wehub-resource-sync e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

320 lines
11 KiB
Python

# Copyright 2026 Alibaba Group Holding Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from fastapi import HTTPException
from kubernetes.client import V1ResourceRequirements
from opensandbox_server.services.constants import SANDBOX_ID_LABEL
from opensandbox_server.services.k8s.k8s_diagnostics import (
K8sDiagnosticsMixin,
_parse_since,
)
class _DiagnosticsService(K8sDiagnosticsMixin):
def __init__(self, pods):
self.namespace = "sandbox-system"
self.k8s_client = MagicMock()
self.k8s_client.list_pods.return_value = pods
self.core_v1 = MagicMock()
self.k8s_client.get_core_v1_api.return_value = self.core_v1
def _status(
*,
running=None,
waiting=None,
terminated=None,
last_terminated=None,
):
return SimpleNamespace(
name="main",
ready=True,
restart_count=2,
image="python:3.12",
state=SimpleNamespace(
running=running,
waiting=waiting,
terminated=terminated,
),
last_state=SimpleNamespace(terminated=last_terminated),
)
def _pod(container_statuses=None, init_container_statuses=None, conditions=None):
return SimpleNamespace(
metadata=SimpleNamespace(
name="pod-1",
namespace="sandbox-system",
labels={SANDBOX_ID_LABEL: "sbx-1", "app": "opensandbox"},
),
spec=SimpleNamespace(
node_name="node-1",
runtime_class_name="gvisor",
containers=[
SimpleNamespace(
name="main",
resources=V1ResourceRequirements(
requests={"cpu": "500m"},
limits={"memory": "512Mi"},
),
)
],
),
status=SimpleNamespace(
phase="Running",
pod_ip="10.1.2.3",
host_ip="192.168.1.10",
start_time="2026-01-01T00:00:00Z",
container_statuses=container_statuses or [],
init_container_statuses=init_container_statuses or [],
conditions=conditions or [],
),
)
def test_parse_since_supports_units_and_default() -> None:
assert _parse_since("5m") == 300
assert _parse_since("2 h") == 7200
assert _parse_since("invalid") == 600
def test_find_pod_uses_label_selector_and_maps_errors() -> None:
service = _DiagnosticsService([_pod()])
pod = service._find_pod_for_sandbox("sbx-1")
assert pod.metadata.name == "pod-1"
service.k8s_client.list_pods.assert_called_once_with(
namespace="sandbox-system",
label_selector=f"{SANDBOX_ID_LABEL}=sbx-1",
)
service.k8s_client.list_pods.return_value = []
with pytest.raises(HTTPException) as not_found:
service._find_pod_for_sandbox("missing")
assert not_found.value.status_code == 404
service.k8s_client.list_pods.side_effect = RuntimeError("api down")
with pytest.raises(HTTPException) as api_error:
service._find_pod_for_sandbox("sbx-1")
assert api_error.value.status_code == 500
def test_get_sandbox_logs_passes_tail_and_since() -> None:
service = _DiagnosticsService([_pod()])
service.core_v1.read_namespaced_pod_log.return_value = "log line"
assert service.get_sandbox_logs("sbx-1", tail=10, since="1h") == "log line"
# Single-container pod uses its only container ("main") as the default.
service.core_v1.read_namespaced_pod_log.assert_called_once_with(
name="pod-1",
namespace="sandbox-system",
container="main",
tail_lines=10,
timestamps=True,
since_seconds=3600,
)
def test_get_sandbox_logs_returns_placeholder_for_empty_output() -> None:
service = _DiagnosticsService([_pod()])
service.core_v1.read_namespaced_pod_log.return_value = ""
assert service.get_sandbox_logs("sbx-1") == "(no logs)"
def _multi_container_pod():
"""Pod modelled after a real OSB sandbox: init + user "sandbox" + egress sidecar."""
return SimpleNamespace(
metadata=SimpleNamespace(
name="pod-1",
namespace="sandbox-system",
labels={SANDBOX_ID_LABEL: "sbx-1"},
),
spec=SimpleNamespace(
node_name="node-1",
runtime_class_name=None,
containers=[
SimpleNamespace(name="sandbox", resources=None),
SimpleNamespace(name="egress", resources=None),
],
init_containers=[SimpleNamespace(name="execd-installer", resources=None)],
),
status=SimpleNamespace(
phase="Running",
pod_ip="10.1.2.3",
host_ip="192.168.1.10",
start_time="2026-01-01T00:00:00Z",
container_statuses=[],
init_container_statuses=[],
conditions=[],
),
)
def test_get_sandbox_logs_defaults_to_sandbox_container_on_multi_container_pod() -> None:
service = _DiagnosticsService([_multi_container_pod()])
service.core_v1.read_namespaced_pod_log.return_value = "user log"
assert service.get_sandbox_logs("sbx-1", tail=5) == "user log"
service.core_v1.read_namespaced_pod_log.assert_called_once_with(
name="pod-1",
namespace="sandbox-system",
container="sandbox",
tail_lines=5,
timestamps=True,
)
def test_get_sandbox_logs_accepts_container_override_for_sidecars() -> None:
service = _DiagnosticsService([_multi_container_pod()])
service.core_v1.read_namespaced_pod_log.return_value = "egress log"
assert service.get_sandbox_logs("sbx-1", container="egress") == "egress log"
service.core_v1.read_namespaced_pod_log.assert_called_once_with(
name="pod-1",
namespace="sandbox-system",
container="egress",
tail_lines=100,
timestamps=True,
)
def test_get_sandbox_logs_allows_init_container_by_name() -> None:
service = _DiagnosticsService([_multi_container_pod()])
service.core_v1.read_namespaced_pod_log.return_value = "init log"
assert service.get_sandbox_logs("sbx-1", container="execd-installer") == "init log"
def test_get_sandbox_logs_unknown_container_returns_404() -> None:
service = _DiagnosticsService([_multi_container_pod()])
with pytest.raises(HTTPException) as exc:
service.get_sandbox_logs("sbx-1", container="not-a-container")
assert exc.value.status_code == 404
assert "not-a-container" in str(exc.value.detail["message"])
def test_get_sandbox_logs_maps_kubernetes_400_to_400_response() -> None:
from kubernetes.client.exceptions import ApiException
service = _DiagnosticsService([_multi_container_pod()])
api_exc = ApiException(status=400, reason="Bad Request")
api_exc.body = (
'a container name must be specified for pod sbx-1-0, '
'choose one of: [sandbox egress]'
)
service.core_v1.read_namespaced_pod_log.side_effect = api_exc
with pytest.raises(HTTPException) as exc:
service.get_sandbox_logs("sbx-1")
assert exc.value.status_code == 400
assert "sandbox" in str(exc.value.detail["message"])
def test_get_sandbox_logs_maps_kubernetes_403_to_forbidden_response() -> None:
from kubernetes.client.exceptions import ApiException
service = _DiagnosticsService([_multi_container_pod()])
api_exc = ApiException(status=403, reason="Forbidden")
api_exc.body = "pods/log forbidden"
service.core_v1.read_namespaced_pod_log.side_effect = api_exc
with pytest.raises(HTTPException) as exc:
service.get_sandbox_logs("sbx-1")
assert exc.value.status_code == 403
def test_get_sandbox_inspect_formats_runtime_statuses_and_resources() -> None:
running_status = _status(running=SimpleNamespace(started_at="2026-01-01T00:00:01Z"))
waiting_status = _status(waiting=SimpleNamespace(reason="ImagePullBackOff", message="pull failed"))
terminated_status = _status(
terminated=SimpleNamespace(exit_code=1, reason="Error", message="boom"),
last_terminated=SimpleNamespace(exit_code=2, reason="PreviousError"),
)
init_status = _status(terminated=SimpleNamespace(exit_code=0, reason="Completed"))
waiting_init = _status(waiting=SimpleNamespace(reason="PodInitializing"))
condition = SimpleNamespace(type="Ready", status="False", reason="ContainersNotReady", message="not ready")
service = _DiagnosticsService(
[
_pod(
container_statuses=[running_status, waiting_status, terminated_status],
init_container_statuses=[init_status, waiting_init],
conditions=[condition],
)
]
)
output = service.get_sandbox_inspect("sbx-1")
assert "Pod Name: pod-1" in output
assert "Runtime Class: gvisor" in output
assert "State: Running (since 2026-01-01T00:00:01Z)" in output
assert "Waiting (ImagePullBackOff)" in output
assert "Terminated (exit=1, reason=Error)" in output
assert "Last State: Terminated (exit=2, reason=PreviousError)" in output
assert "Init Containers:" in output
assert "Ready: False (reason=ContainersNotReady)" in output
assert f"{SANDBOX_ID_LABEL}=sbx-1" in output
assert "Requests: {'cpu': '500m'}" in output
assert "Limits: {'memory': '512Mi'}" in output
def test_get_sandbox_events_formats_events_and_empty_result() -> None:
service = _DiagnosticsService([_pod()])
service.core_v1.list_namespaced_event.return_value = SimpleNamespace(
items=[
SimpleNamespace(
last_timestamp="2026-01-01T00:00:00Z",
event_time=None,
first_timestamp=None,
type="Warning",
reason="Failed",
message="container failed",
),
SimpleNamespace(
last_timestamp=None,
event_time="2026-01-01T00:00:01Z",
first_timestamp=None,
type="Normal",
reason=None,
message=None,
),
]
)
output = service.get_sandbox_events("sbx-1", limit=2)
assert "[2026-01-01T00:00:00Z] Warning" in output
assert "Failed" in output
assert "container failed" in output
assert "N/A" in output
service.core_v1.list_namespaced_event.assert_called_once_with(
namespace="sandbox-system",
field_selector="involvedObject.name=pod-1",
limit=2,
)
service.core_v1.list_namespaced_event.return_value = SimpleNamespace(items=[])
assert service.get_sandbox_events("sbx-1") == "(no events)"