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

3044 lines
116 KiB
Python

# Copyright 2025 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.
import pytest
from types import SimpleNamespace
from datetime import datetime, timezone
from unittest.mock import MagicMock
from fastapi import HTTPException
from kubernetes.client import ApiException
from opensandbox_server.api.schema import (
ImageSpec,
ImageAuth,
NetworkPolicy,
NetworkRule,
PlatformSpec,
)
from opensandbox_server.config import (
AppConfig,
EGRESS_MODE_DNS,
EGRESS_MODE_DNS_NFT,
EgressConfig,
ExecdInitResources,
KubernetesRuntimeConfig,
RuntimeConfig,
)
from opensandbox_server.services.constants import (
OPEN_SANDBOX_EGRESS_AUTH_HEADER,
OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT,
OPENSANDBOX_RUNTIME_MOUNT_PATH,
OPENSANDBOX_RUNTIME_VOLUME_NAME,
SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY,
)
from opensandbox_server.services.k8s.batchsandbox_provider import BatchSandboxProvider
from opensandbox_server.services.constants import OPENSANDBOX_EGRESS_TOKEN
from opensandbox_server.services.k8s.image_pull_secret_helper import IMAGE_AUTH_SECRET_PREFIX
from opensandbox_server.services.k8s.volume_helper import apply_volumes_to_pod_spec
def _app_config_with_template(template_file_path: str) -> AppConfig:
"""Build an AppConfig with a batchsandbox_template_file set."""
return AppConfig(
runtime=RuntimeConfig(type="kubernetes", execd_image="execd:test"),
kubernetes=KubernetesRuntimeConfig(
namespace="test-ns",
batchsandbox_template_file=template_file_path,
),
)
def _app_config_with_execd_resources(execd_init_resources: ExecdInitResources) -> AppConfig:
"""Build an AppConfig with execd_init_resources set."""
return AppConfig(
runtime=RuntimeConfig(type="kubernetes", execd_image="execd:test"),
kubernetes=KubernetesRuntimeConfig(
namespace="test-ns",
execd_init_resources=execd_init_resources,
),
)
def _app_config_with_image_pull_policy(image_pull_policy: str) -> AppConfig:
"""Build an AppConfig with image_pull_policy set."""
return AppConfig(
runtime=RuntimeConfig(type="kubernetes", execd_image="execd:test"),
kubernetes=KubernetesRuntimeConfig(
namespace="test-ns",
image_pull_policy=image_pull_policy,
),
)
def _app_config_with_egress_disable_ipv6(disable_ipv6: bool = True) -> AppConfig:
"""Build an AppConfig with ``egress.disable_ipv6`` set (privileged execd init when egress is used)."""
return AppConfig(
runtime=RuntimeConfig(type="kubernetes", execd_image="execd:test"),
kubernetes=KubernetesRuntimeConfig(namespace="test-ns"),
egress=EgressConfig(disable_ipv6=disable_ipv6),
)
class TestBatchSandboxProvider:
# ===== Initialization Tests =====
def test_init_without_template_creates_provider(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
assert provider.k8s_client == mock_k8s_client
assert provider.template_manager._template is None
assert provider.group == "sandbox.opensandbox.io"
assert provider.version == "v1alpha1"
assert provider.plural == "batchsandboxes"
def test_init_with_template_loads_template(self, mock_k8s_client, tmp_path):
template_file = tmp_path / "template.yaml"
template_file.write_text("spec:\n replicas: 1")
provider = BatchSandboxProvider(
mock_k8s_client, _app_config_with_template(str(template_file))
)
assert provider.template_manager._template is not None
def test_init_sets_crd_constants_correctly(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
assert provider.group == "sandbox.opensandbox.io"
assert provider.version == "v1alpha1"
assert provider.plural == "batchsandboxes"
# ===== Workload Creation Tests =====
def test_create_workload_builds_correct_manifest(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
expires_at = datetime(2025, 12, 31, 10, 0, 0, tzinfo=timezone.utc)
result = provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={"FOO": "bar"},
resource_limits={"cpu": "1", "memory": "1Gi"},
labels={"opensandbox.io/id": "test-id"},
expires_at=expires_at,
execd_image="execd:latest",
)
assert result == {"name": "test-id", "uid": "test-uid", "apiVersion": "sandbox.opensandbox.io/v1alpha1", "kind": "BatchSandbox"}
# Verify API call
call_args = mock_k8s_client.create_custom_object.call_args
body = call_args.kwargs["body"]
assert body["apiVersion"] == "sandbox.opensandbox.io/v1alpha1"
assert body["kind"] == "BatchSandbox"
assert body["metadata"]["name"] == "test-id"
assert body["metadata"]["namespace"] == "test-ns"
assert body["spec"]["replicas"] == 1
assert body["spec"]["expireTime"] == "2025-12-31T10:00:00+00:00"
assert "template" in body["spec"]
assert body["spec"]["template"]["spec"]["automountServiceAccountToken"] is False
assert "initContainers" in body["spec"]["template"]["spec"]
assert "containers" in body["spec"]["template"]["spec"]
assert "volumes" in body["spec"]["template"]["spec"]
def test_create_workload_injects_platform_node_selector(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={"cpu": "1", "memory": "1Gi"},
labels={"opensandbox.io/id": "test-id"},
expires_at=None,
execd_image="execd:latest",
platform=PlatformSpec(os="linux", arch="arm64"),
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
node_selector = body["spec"]["template"]["spec"]["nodeSelector"]
assert node_selector["kubernetes.io/os"] == "linux"
assert node_selector["kubernetes.io/arch"] == "arm64"
def test_create_workload_windows_profile_uses_windows_runtime_shape(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="dockurr/windows:latest"),
entrypoint=["cmd", "/c", "echo hello"],
env={"VERSION": "11"},
resource_limits={"cpu": "4", "memory": "8G", "disk": "64G"},
labels={"opensandbox.io/id": "test-id"},
expires_at=None,
execd_image="execd:latest",
platform=PlatformSpec(os="windows", arch="amd64"),
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
# windows profile should enforce requested arch, but not force os=windows.
node_selector = pod_spec.get("nodeSelector", {})
assert node_selector["kubernetes.io/arch"] == "amd64"
assert "kubernetes.io/os" not in node_selector
init_container = pod_spec["initContainers"][0]
assert init_container["command"] == ["/bin/sh", "-c"]
assert "install.bat" in init_container["args"][0]
assert "execd.exe" in init_container["args"][0]
main_container = pod_spec["containers"][0]
assert main_container["command"] == ["cmd", "/c", "echo hello"]
# Resources include QEMU memory overhead (8G + 2Gi overhead = 10Gi)
assert main_container["resources"]["limits"]["cpu"] == "4"
assert main_container["resources"]["limits"]["memory"] == "10Gi"
env_dict = {item["name"]: item["value"] for item in main_container.get("env", [])}
assert env_dict["VERSION"] == "11"
assert env_dict["CPU_CORES"] == "4"
assert env_dict["RAM_SIZE"] == "8G"
assert env_dict["DISK_SIZE"] == "64G"
assert env_dict["USER_PORTS"] == "44772,8080,3389,8006"
volume_names = {volume["name"] for volume in pod_spec.get("volumes", [])}
assert "opensandbox-win-oem" in volume_names
assert "opensandbox-win-kvm" in volume_names
assert "opensandbox-win-tun" in volume_names
def test_create_workload_windows_profile_default_entrypoint_uses_image_entrypoint(
self, mock_k8s_client
):
"""When entrypoint is the SDK default, command is removed so image ENTRYPOINT runs."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="dockurr/windows:latest"),
entrypoint=["tail", "-f", "/dev/null"],
env={"VERSION": "11"},
resource_limits={"cpu": "4", "memory": "8G", "disk": "64G"},
labels={"opensandbox.io/id": "test-id"},
expires_at=None,
execd_image="execd:latest",
platform=PlatformSpec(os="windows", arch="amd64"),
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
main_container = pod_spec["containers"][0]
# No command set - image default ENTRYPOINT will be used
assert "command" not in main_container
assert "args" not in main_container
def test_create_workload_windows_profile_merges_user_ports(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="dockurr/windows:latest"),
entrypoint=["cmd", "/c", "echo hello"],
env={"VERSION": "11", "USER_PORTS": "3000,44772"},
resource_limits={"cpu": "4", "memory": "8G", "disk": "64G"},
labels={"opensandbox.io/id": "test-id"},
expires_at=None,
execd_image="execd:latest",
platform=PlatformSpec(os="windows", arch="amd64"),
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
main_container = pod_spec["containers"][0]
env_dict = {item["name"]: item["value"] for item in main_container.get("env", [])}
assert env_dict["USER_PORTS"] == "3000,44772,8080,3389,8006"
def test_create_workload_windows_profile_rejects_arch_conflict_with_template_selector(
self, mock_k8s_client, tmp_path
):
template_file = tmp_path / "template.yaml"
template_file.write_text(
"""
spec:
template:
spec:
nodeSelector:
kubernetes.io/arch: arm64
"""
)
provider = BatchSandboxProvider(
mock_k8s_client, _app_config_with_template(str(template_file))
)
with pytest.raises(ValueError, match="platform conflict with template nodeSelector"):
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="dockurr/windows:latest"),
entrypoint=["cmd", "/c", "echo hello"],
env={"VERSION": "11"},
resource_limits={"cpu": "4", "memory": "8G", "disk": "64G"},
labels={"opensandbox.io/id": "test-id"},
expires_at=None,
execd_image="execd:latest",
platform=PlatformSpec(os="windows", arch="amd64"),
)
def test_create_workload_rejects_platform_conflict_with_template_selector(
self, mock_k8s_client, tmp_path
):
template_file = tmp_path / "template.yaml"
template_file.write_text(
"""
spec:
template:
spec:
nodeSelector:
kubernetes.io/os: linux
kubernetes.io/arch: amd64
"""
)
provider = BatchSandboxProvider(
mock_k8s_client, _app_config_with_template(str(template_file))
)
with pytest.raises(ValueError, match="platform conflict with template nodeSelector"):
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={"cpu": "1", "memory": "1Gi"},
labels={"opensandbox.io/id": "test-id"},
expires_at=None,
execd_image="execd:latest",
platform=PlatformSpec(os="linux", arch="arm64"),
)
def test_create_workload_rejects_platform_conflict_with_template_node_affinity(
self, mock_k8s_client, tmp_path
):
template_file = tmp_path / "template.yaml"
template_file.write_text(
"""
spec:
template:
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
"""
)
provider = BatchSandboxProvider(
mock_k8s_client, _app_config_with_template(str(template_file))
)
with pytest.raises(ValueError, match="platform conflict with template nodeAffinity"):
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={"cpu": "1", "memory": "1Gi"},
labels={"opensandbox.io/id": "test-id"},
expires_at=None,
execd_image="execd:latest",
platform=PlatformSpec(os="linux", arch="arm64"),
)
def test_create_workload_builds_execd_init_container(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test", "uid": "uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:test",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
init_container = body["spec"]["template"]["spec"]["initContainers"][0]
assert init_container["name"] == "execd-installer"
assert init_container["image"] == "execd:test"
assert init_container["command"] == ["/bin/sh", "-c"]
assert "bootstrap.sh" in init_container["args"][0]
assert init_container["volumeMounts"][0]["name"] == "opensandbox-bin"
# No resources configured: resources field should be absent
assert "resources" not in init_container
def test_create_workload_init_container_with_configured_resources(self, mock_k8s_client):
provider = BatchSandboxProvider(
mock_k8s_client,
_app_config_with_execd_resources(
ExecdInitResources(
limits={"cpu": "100m", "memory": "128Mi"},
requests={"cpu": "50m", "memory": "64Mi"},
)
),
)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test", "uid": "uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:test",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
init_container = body["spec"]["template"]["spec"]["initContainers"][0]
assert init_container["resources"]["limits"] == {"cpu": "100m", "memory": "128Mi"}
assert init_container["resources"]["requests"] == {"cpu": "50m", "memory": "64Mi"}
def test_create_workload_sets_configured_image_pull_policy(self, mock_k8s_client):
provider = BatchSandboxProvider(
mock_k8s_client,
_app_config_with_image_pull_policy("Always"),
)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test", "uid": "uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:test",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
main_container = body["spec"]["template"]["spec"]["containers"][0]
assert main_container["imagePullPolicy"] == "Always"
def test_create_workload_wraps_entrypoint_with_bootstrap(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "sandbox-test", "uid": "uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/usr/bin/python", "app.py"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
main_container = body["spec"]["template"]["spec"]["containers"][0]
assert main_container["command"] == [
"/opt/opensandbox/bootstrap.sh",
"/usr/bin/python",
"app.py",
]
def test_create_workload_converts_env_to_list(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "sandbox-test", "uid": "uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={"FOO": "bar", "BAZ": "qux"},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
env_vars = body["spec"]["template"]["spec"]["containers"][0]["env"]
# Should have user env vars plus EXECD
assert len(env_vars) == 3
env_dict = {e["name"]: e["value"] for e in env_vars}
assert env_dict["FOO"] == "bar"
assert env_dict["BAZ"] == "qux"
# Verify EXECD is automatically injected
assert env_dict["EXECD"] == "/opt/opensandbox/execd"
def test_create_workload_merges_template_volumes_and_mounts(self, mock_k8s_client, tmp_path):
template_file = tmp_path / "template.yaml"
template_file.write_text(
"""
spec:
template:
spec:
volumes:
- name: sandbox-shared-data
emptyDir: {}
containers:
- name: sandbox
image: ubuntu:latest
volumeMounts:
- name: sandbox-shared-data
mountPath: /data
"""
)
provider = BatchSandboxProvider(
mock_k8s_client, _app_config_with_template(str(template_file))
)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "sandbox-test", "uid": "uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
spec = body["spec"]["template"]["spec"]
volume_names = [v["name"] for v in spec["volumes"]]
assert "sandbox-shared-data" in volume_names
assert "opensandbox-bin" in volume_names
# Runtime container should stay intact (template image should not override)
container = spec["containers"][0]
assert container["name"] == "sandbox"
assert container["image"] == "python:3.11"
mount_names = [m["name"] for m in container["volumeMounts"]]
assert "sandbox-shared-data" in mount_names
assert "opensandbox-bin" in mount_names
def test_create_workload_dedupes_template_volume_and_mount_names(
self, mock_k8s_client, tmp_path
):
template_file = tmp_path / "template.yaml"
template_file.write_text(
"""
spec:
template:
spec:
volumes:
- name: opensandbox-bin
emptyDir: {}
- name: sandbox-shared-data
emptyDir: {}
containers:
- name: sandbox
volumeMounts:
- name: opensandbox-bin
mountPath: /opt/opensandbox
- name: sandbox-shared-data
mountPath: /data
"""
)
provider = BatchSandboxProvider(
mock_k8s_client, _app_config_with_template(str(template_file))
)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "sandbox-test", "uid": "uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
spec = body["spec"]["template"]["spec"]
volume_names = [v["name"] for v in spec["volumes"]]
assert volume_names.count("opensandbox-bin") == 1
assert "sandbox-shared-data" in volume_names
mount_names = [m["name"] for m in spec["containers"][0]["volumeMounts"]]
assert mount_names.count("opensandbox-bin") == 1
assert "sandbox-shared-data" in mount_names
def test_create_workload_sets_resource_limits_and_requests(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "sandbox-test", "uid": "uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={"cpu": "1", "memory": "1Gi"},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
resources = body["spec"]["template"]["spec"]["containers"][0]["resources"]
assert resources["limits"] == {"cpu": "1", "memory": "1Gi"}
assert resources["requests"] == {"cpu": "1", "memory": "1Gi"}
def test_create_workload_uses_separate_resource_requests(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "sandbox-test", "uid": "uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={"cpu": "2", "memory": "2Gi"},
resource_requests={"cpu": "500m", "memory": "512Mi"},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
resources = body["spec"]["template"]["spec"]["containers"][0]["resources"]
assert resources["limits"] == {"cpu": "2", "memory": "2Gi"}
assert resources["requests"] == {"cpu": "500m", "memory": "512Mi"}
def test_create_workload_handles_empty_resource_limits(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "sandbox-test", "uid": "uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
container = body["spec"]["template"]["spec"]["containers"][0]
assert "resources" not in container
def test_create_workload_translates_gpu_to_nvidia_extended_resource(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "sandbox-test", "uid": "uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={"cpu": "1", "memory": "1Gi", "gpu": "2"},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
resources = body["spec"]["template"]["spec"]["containers"][0]["resources"]
assert resources["limits"]["nvidia.com/gpu"] == "2"
assert resources["requests"]["nvidia.com/gpu"] == "2"
# Raw key must not leak through as an unknown extended resource.
assert "gpu" not in resources["limits"]
assert "gpu" not in resources["requests"]
def test_create_workload_without_gpu_omits_nvidia_extended_resource(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "sandbox-test", "uid": "uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={"cpu": "1", "memory": "1Gi"},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
resources = body["spec"]["template"]["spec"]["containers"][0]["resources"]
assert "nvidia.com/gpu" not in resources["limits"]
assert "nvidia.com/gpu" not in resources["requests"]
def test_create_workload_rejects_gpu_all_sentinel(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
with pytest.raises(HTTPException) as excinfo:
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={"cpu": "1", "gpu": "all"},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
)
assert excinfo.value.status_code == 400
# ===== Workload Query Tests =====
def test_get_workload_finds_existing_sandbox(
self, mock_k8s_client, mock_batchsandbox_list_response
):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = mock_batchsandbox_list_response["items"][0]
result = provider.get_workload("test-id", "test-ns")
assert result is not None
assert result["metadata"]["name"] == "test-id"
def test_get_workload_returns_none_when_not_found(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = None
result = provider.get_workload("test-id", "test-ns")
assert result is None
def test_get_workload_falls_back_to_legacy_name(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.side_effect = [
None,
{"metadata": {"name": "sandbox-test-id"}},
]
result = provider.get_workload("test-id", "test-ns")
assert result["metadata"]["name"] == "sandbox-test-id"
assert mock_k8s_client.get_custom_object.call_args_list[0].kwargs["name"] == "test-id"
assert (
mock_k8s_client.get_custom_object.call_args_list[1].kwargs["name"] == "sandbox-test-id"
)
def test_get_workload_handles_404_gracefully(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = None
result = provider.get_workload("test-id", "test-ns")
assert result is None
def test_get_workload_reraises_non_404_exceptions(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
# Mock 500 exception
error = ApiException(status=500)
mock_k8s_client.get_custom_object.side_effect = error
with pytest.raises(ApiException) as exc_info:
provider.get_workload("test-id", "test-ns")
assert exc_info.value.status == 500
def test_get_workload_prefers_informer_cache(self, mock_k8s_client):
cached = {"metadata": {"name": "test-id"}}
mock_k8s_client.get_custom_object.return_value = cached
provider = BatchSandboxProvider(mock_k8s_client)
result = provider.get_workload("test-id", "test-ns")
assert result == cached
mock_k8s_client.get_custom_object.assert_called()
def test_get_workload_logs_unexpected_errors(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.side_effect = RuntimeError("Unexpected")
with pytest.raises(RuntimeError, match="Unexpected"):
provider.get_workload("test-id", "test-ns")
def test_create_workload_updates_informer_cache(self, mock_k8s_client):
created_body = {"metadata": {"name": "test-id", "uid": "test-uid"}}
mock_k8s_client.create_custom_object.return_value = created_body
provider = BatchSandboxProvider(mock_k8s_client)
expires_at = datetime(2025, 12, 31, tzinfo=timezone.utc)
result = provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={"FOO": "bar"},
resource_limits={"cpu": "1", "memory": "1Gi"},
labels={"opensandbox.io/id": "test-id"},
expires_at=expires_at,
execd_image="execd:latest",
)
assert result == {"name": "test-id", "uid": "test-uid", "apiVersion": "sandbox.opensandbox.io/v1alpha1", "kind": "BatchSandbox"}
# ===== Workload List Tests =====
def test_list_workloads_returns_items(self, mock_k8s_client, mock_batchsandbox_list_response):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.list_custom_objects.return_value = mock_batchsandbox_list_response["items"]
result = provider.list_workloads("test-ns", "opensandbox.io/id")
assert len(result) == 1
assert result[0]["metadata"]["name"] == "test-id"
def test_list_workloads_returns_empty_on_404(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.list_custom_objects.return_value = []
result = provider.list_workloads("test-ns", "opensandbox.io/id")
assert result == []
# ===== Workload Deletion Tests =====
def test_delete_workload_deletes_existing_sandbox(
self, mock_k8s_client, mock_batchsandbox_list_response
):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = mock_batchsandbox_list_response["items"][0]
provider.delete_workload("test-id", "test-ns")
mock_k8s_client.delete_custom_object.assert_called_once_with(
group="sandbox.opensandbox.io",
version="v1alpha1",
namespace="test-ns",
plural="batchsandboxes",
name="test-id",
grace_period_seconds=0,
)
def test_delete_workload_raises_when_not_found(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = None
with pytest.raises(Exception) as exc_info:
provider.delete_workload("test-id", "test-ns")
assert "not found" in str(exc_info.value)
def test_delete_workload_sets_grace_period_zero(
self, mock_k8s_client, mock_batchsandbox_list_response
):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = mock_batchsandbox_list_response["items"][0]
provider.delete_workload("test-id", "test-ns")
call_kwargs = mock_k8s_client.delete_custom_object.call_args.kwargs
assert call_kwargs["grace_period_seconds"] == 0
# ===== Expiration Time Management Tests =====
def test_update_expiration_patches_spec(self, mock_k8s_client, mock_batchsandbox_list_response):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = mock_batchsandbox_list_response["items"][0]
expires_at = datetime(2025, 12, 31, 0, 0, 0, tzinfo=timezone.utc)
provider.update_expiration("test-id", "test-ns", expires_at)
call_kwargs = mock_k8s_client.patch_custom_object.call_args.kwargs
assert call_kwargs["body"] == {"spec": {"expireTime": "2025-12-31T00:00:00+00:00"}}
def test_get_expiration_parses_iso_format(self):
provider = BatchSandboxProvider(MagicMock())
workload = {"spec": {"expireTime": "2025-12-31T10:00:00+00:00"}}
result = provider.get_expiration(workload)
assert result == datetime(2025, 12, 31, 10, 0, 0, tzinfo=timezone.utc)
def test_get_expiration_handles_z_suffix(self):
provider = BatchSandboxProvider(MagicMock())
workload = {"spec": {"expireTime": "2025-12-31T10:00:00Z"}}
result = provider.get_expiration(workload)
assert result == datetime(2025, 12, 31, 10, 0, 0, tzinfo=timezone.utc)
def test_get_expiration_returns_none_on_invalid_format(self):
provider = BatchSandboxProvider(MagicMock())
workload = {"spec": {"expireTime": "invalid-date"}}
# Should return None and not raise exception
result = provider.get_expiration(workload)
assert result is None
def test_get_expiration_returns_none_when_missing(self):
provider = BatchSandboxProvider(MagicMock())
workload = {"spec": {}}
result = provider.get_expiration(workload)
assert result is None
# ===== Status Retrieval Tests =====
def test_get_status_running_with_ip(self):
provider = BatchSandboxProvider(MagicMock())
workload = {
"status": {"replicas": 1, "ready": 1, "allocated": 1},
"metadata": {
"annotations": {"sandbox.opensandbox.io/endpoints": '["10.0.0.1"]'},
"creationTimestamp": "2025-12-24T10:00:00Z",
},
}
result = provider.get_status(workload)
assert result["state"] == "Running"
assert result["reason"] == "POD_READY_WITH_IP"
assert "IP" in result["message"]
def test_get_status_allocated_with_ip_not_ready(self):
provider = BatchSandboxProvider(MagicMock())
workload = {
"status": {"replicas": 1, "ready": 0, "allocated": 1},
"metadata": {
"annotations": {"sandbox.opensandbox.io/endpoints": '["10.0.0.1"]'},
"creationTimestamp": "2025-12-24T10:00:00Z",
},
}
result = provider.get_status(workload)
assert result["state"] == "Allocated"
assert result["reason"] == "IP_ASSIGNED"
def test_get_status_pending_scheduled(self):
provider = BatchSandboxProvider(MagicMock())
workload = {
"status": {"replicas": 1, "ready": 0, "allocated": 1},
"metadata": {"creationTimestamp": "2025-12-24T10:00:00Z"},
}
result = provider.get_status(workload)
assert result["state"] == "Pending"
assert result["reason"] == "POD_SCHEDULED"
def test_get_status_pending_when_endpoints_invalid_json(self):
provider = BatchSandboxProvider(MagicMock())
workload = {
"status": {"replicas": 1, "ready": 0, "allocated": 1},
"metadata": {
"annotations": {"sandbox.opensandbox.io/endpoints": "invalid-json"},
"creationTimestamp": "2025-12-24T10:00:00Z",
},
}
result = provider.get_status(workload)
assert result["state"] == "Pending"
assert result["reason"] == "POD_SCHEDULED"
def test_get_status_pending_when_endpoints_empty_array(self):
provider = BatchSandboxProvider(MagicMock())
workload = {
"status": {"replicas": 1, "ready": 0, "allocated": 1},
"metadata": {
"annotations": {"sandbox.opensandbox.io/endpoints": "[]"},
"creationTimestamp": "2025-12-24T10:00:00Z",
},
}
result = provider.get_status(workload)
assert result["state"] == "Pending"
assert result["reason"] == "POD_SCHEDULED"
def test_get_status_pending_unallocated(self):
provider = BatchSandboxProvider(MagicMock())
workload = {
"status": {"replicas": 1, "ready": 0, "allocated": 0},
"metadata": {"creationTimestamp": "2025-12-24T10:00:00Z"},
}
result = provider.get_status(workload)
assert result["state"] == "Pending"
assert result["reason"] == "BATCHSANDBOX_PENDING"
def test_get_status_returns_failed_when_pod_unschedulable(self):
mock_k8s_client = MagicMock()
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.list_pods.return_value = [
SimpleNamespace(
status=SimpleNamespace(
phase="Pending",
pod_ip=None,
conditions=[
SimpleNamespace(
type="PodScheduled",
status="False",
reason="Unschedulable",
message="0/1 nodes are available: 1 node(s) didn't match Pod's node affinity.",
)
],
)
)
]
workload = {
"spec": {
"template": {
"spec": {
"nodeSelector": {
"kubernetes.io/os": "linux",
"kubernetes.io/arch": "arm64",
}
}
}
},
"status": {
"replicas": 1,
"ready": 0,
"allocated": 0,
"selector": "app=sandbox",
},
"metadata": {
"namespace": "test-ns",
"creationTimestamp": "2025-12-24T10:00:00Z",
},
}
result = provider.get_status(workload)
assert result["state"] == "Failed"
assert result["reason"] == "POD_PLATFORM_UNSCHEDULABLE"
assert "didn't match Pod's node affinity" in result["message"]
def test_get_status_keeps_pending_for_generic_failed_scheduling(self):
mock_k8s_client = MagicMock()
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.list_pods.return_value = [
SimpleNamespace(
status=SimpleNamespace(
phase="Pending",
pod_ip=None,
conditions=[
SimpleNamespace(
type="PodScheduled",
status="False",
reason="Unschedulable",
message="0/1 nodes are available: 1 Insufficient cpu.",
)
],
)
)
]
workload = {
"status": {
"replicas": 1,
"ready": 0,
"allocated": 0,
"selector": "app=sandbox",
},
"metadata": {
"namespace": "test-ns",
"creationTimestamp": "2025-12-24T10:00:00Z",
},
}
result = provider.get_status(workload)
assert result["state"] == "Pending"
assert result["reason"] == "BATCHSANDBOX_PENDING"
def test_get_status_keeps_pending_when_non_platform_affinity_mismatch(self):
mock_k8s_client = MagicMock()
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.list_pods.return_value = [
SimpleNamespace(
status=SimpleNamespace(
phase="Pending",
pod_ip=None,
conditions=[
SimpleNamespace(
type="PodScheduled",
status="False",
reason="Unschedulable",
message="0/1 nodes are available: 1 node(s) didn't match Pod's node affinity.",
)
],
)
)
]
workload = {
"spec": {
"template": {
"spec": {
"nodeSelector": {
"kubernetes.io/os": "linux",
"kubernetes.io/arch": "arm64",
"zone": "cn-hz-a",
}
}
}
},
"status": {
"replicas": 1,
"ready": 0,
"allocated": 0,
"selector": "app=sandbox",
},
"metadata": {
"namespace": "test-ns",
"creationTimestamp": "2025-12-24T10:00:00Z",
},
}
result = provider.get_status(workload)
assert result["state"] == "Pending"
assert result["reason"] == "BATCHSANDBOX_PENDING"
def test_get_status_keeps_pending_for_mixed_capacity_and_affinity_message(self):
mock_k8s_client = MagicMock()
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.list_pods.return_value = [
SimpleNamespace(
status=SimpleNamespace(
phase="Pending",
pod_ip=None,
conditions=[
SimpleNamespace(
type="PodScheduled",
status="False",
reason="Unschedulable",
message=(
"0/2 nodes are available: 1 Insufficient cpu, "
"1 node(s) didn't match Pod's node affinity/selector."
),
)
],
)
)
]
workload = {
"spec": {
"template": {
"spec": {
"nodeSelector": {
"kubernetes.io/os": "linux",
"kubernetes.io/arch": "arm64",
}
}
}
},
"status": {
"replicas": 1,
"ready": 0,
"allocated": 0,
"selector": "app=sandbox",
},
"metadata": {
"namespace": "test-ns",
"creationTimestamp": "2025-12-24T10:00:00Z",
},
}
result = provider.get_status(workload)
assert result["state"] == "Pending"
assert result["reason"] == "BATCHSANDBOX_PENDING"
# ===== Endpoint Information Tests =====
def test_get_endpoint_info_parses_json_annotation(self):
provider = BatchSandboxProvider(MagicMock())
workload = {
"metadata": {"annotations": {"sandbox.opensandbox.io/endpoints": '["10.0.0.1"]'}}
}
result = provider.get_endpoint_info(workload, 8080, "sandbox-123")
assert result.endpoint == "10.0.0.1:8080"
assert result.headers is None
def test_get_endpoint_info_uses_first_ip(self):
provider = BatchSandboxProvider(MagicMock())
workload = {
"metadata": {
"annotations": {"sandbox.opensandbox.io/endpoints": '["10.0.0.1", "10.0.0.2"]'}
}
}
result = provider.get_endpoint_info(workload, 8080, "sandbox-123")
assert result.endpoint == "10.0.0.1:8080"
assert result.headers is None
def test_get_endpoint_info_returns_none_when_missing(self):
provider = BatchSandboxProvider(MagicMock())
workload = {"metadata": {"annotations": {}}}
result = provider.get_endpoint_info(workload, 8080, "sandbox-123")
assert result is None
def test_get_endpoint_info_returns_none_on_invalid_json(self):
provider = BatchSandboxProvider(MagicMock())
workload = {
"metadata": {"annotations": {"sandbox.opensandbox.io/endpoints": "invalid-json"}}
}
result = provider.get_endpoint_info(workload, 8080, "sandbox-123")
assert result is None
def test_get_endpoint_info_returns_none_on_empty_array(self):
provider = BatchSandboxProvider(MagicMock())
workload = {"metadata": {"annotations": {"sandbox.opensandbox.io/endpoints": "[]"}}}
result = provider.get_endpoint_info(workload, 8080, "sandbox-123")
assert result is None
# ===== Pool-based Creation Tests =====
def test_create_workload_poolref_ignores_image_spec(self, mock_k8s_client):
"""
Test that pool-based creation ignores image_spec parameter.
Pool already defines the image, so image_spec is not used even if provided.
This verifies backward compatibility - no error is raised.
"""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "sandbox-test-id", "uid": "test-uid"}
}
result = provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["python", "app.py"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
extensions={"poolRef": "my-pool"},
)
# Should succeed and return workload info
assert result == {"name": "sandbox-test-id", "uid": "test-uid", "apiVersion": "sandbox.opensandbox.io/v1alpha1", "kind": "BatchSandbox"}
# Verify poolRef is used
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
assert body["spec"]["poolRef"] == "my-pool"
def test_create_workload_poolref_ignores_resource_limits(self, mock_k8s_client):
"""
Test that pool-based creation ignores resource_limits parameter.
Pool already defines the resources, so resource_limits is not used even if provided.
This verifies backward compatibility - no error is raised.
"""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "sandbox-test-id", "uid": "test-uid"}
}
result = provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri=""),
entrypoint=["python", "app.py"],
env={},
resource_limits={"cpu": "1", "memory": "1Gi"},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
extensions={"poolRef": "my-pool"},
)
# Should succeed and return workload info
assert result == {"name": "sandbox-test-id", "uid": "test-uid", "apiVersion": "sandbox.opensandbox.io/v1alpha1", "kind": "BatchSandbox"}
# Verify poolRef is used
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
assert body["spec"]["poolRef"] == "my-pool"
def test_create_workload_poolref_allows_entrypoint_and_env(self, mock_k8s_client):
"""
Test that pool-based creation allows customizing entrypoint and env.
Verifies taskTemplate structure is correctly generated with user's entrypoint and env.
"""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "sandbox-test-id", "uid": "test-uid"}
}
result = provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri=""),
entrypoint=["python", "app.py"],
env={"FOO": "bar"},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
extensions={"poolRef": "my-pool"},
)
assert result == {"name": "sandbox-test-id", "uid": "test-uid", "apiVersion": "sandbox.opensandbox.io/v1alpha1", "kind": "BatchSandbox"}
# Verify the call
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
assert body["spec"]["poolRef"] == "my-pool"
assert "taskTemplate" in body["spec"]
# Verify taskTemplate structure
task_template = body["spec"]["taskTemplate"]
assert "spec" in task_template
assert "process" in task_template["spec"]
command = task_template["spec"]["process"]["command"]
assert command[0] == "/bin/sh"
assert command[1] == "-c"
# Command should contain bootstrap.sh execution
# Example: /opt/opensandbox/bootstrap.sh python app.py &
assert "/opt/opensandbox/bootstrap.sh python app.py" in command[2]
assert command[2].endswith(" &")
assert task_template["spec"]["process"]["env"] == [{"name": "FOO", "value": "bar"}]
def test_build_task_template_with_env(self, mock_k8s_client):
"""
Test _build_task_template with environment variables.
Verifies:
- Command uses shell wrapper: /bin/sh -c "..."
- Entrypoint executed via bootstrap.sh in background (&)
- Env list formatted correctly for K8s
Generated command example:
/bin/sh -c "/opt/opensandbox/bootstrap.sh /usr/bin/python app.py &"
"""
provider = BatchSandboxProvider(mock_k8s_client)
result = provider._build_task_template(
entrypoint=["/usr/bin/python", "app.py"], env={"KEY1": "value1", "KEY2": "value2"}
)
assert "spec" in result
assert "process" in result["spec"]
process_task = result["spec"]["process"]
# Verify command structure
command = process_task["command"]
assert command[0] == "/bin/sh"
assert command[1] == "-c"
# Should execute via bootstrap.sh in background (&)
assert "/opt/opensandbox/bootstrap.sh" in command[2]
assert "/usr/bin/python" in command[2]
assert "app.py" in command[2]
# Should end with & (run in background)
assert command[2].endswith("&")
# Verify env list
assert process_task["env"] == [
{"name": "KEY1", "value": "value1"},
{"name": "KEY2", "value": "value2"},
]
def test_build_task_template_without_env(self, mock_k8s_client):
"""
Test _build_task_template without environment variables.
Verifies command is wrapped in shell and executes via bootstrap.sh in background.
Generated command example:
/bin/sh -c "/opt/opensandbox/bootstrap.sh /usr/bin/python app.py &"
"""
provider = BatchSandboxProvider(mock_k8s_client)
result = provider._build_task_template(entrypoint=["/usr/bin/python", "app.py"], env={})
assert "spec" in result
assert "process" in result["spec"]
process_task = result["spec"]["process"]
assert process_task["env"] == []
# Without env, command directly calls bootstrap.sh in background
command = process_task["command"]
assert command[0] == "/bin/sh"
assert command[1] == "-c"
# Check escaped entrypoint
assert "/opt/opensandbox/bootstrap.sh" in command[2]
assert "/usr/bin/python" in command[2]
assert "app.py" in command[2]
assert command[2].endswith(" &")
def test_build_task_template_uses_default_env_path(self, mock_k8s_client):
"""
Test that taskTemplate executes bootstrap.sh properly.
Verifies:
- Entrypoint is properly escaped
- Command runs in background
"""
provider = BatchSandboxProvider(mock_k8s_client)
result = provider._build_task_template(
entrypoint=["python", "app.py"], env={"TEST_VAR": "test_value"}
)
command = result["spec"]["process"]["command"][2]
# Should execute bootstrap.sh in background
assert "/opt/opensandbox/bootstrap.sh" in command
assert "python" in command
assert "app.py" in command
assert command.endswith(" &")
def test_build_task_template_escapes_special_characters(self, mock_k8s_client):
"""
Test that taskTemplate properly escapes arguments with spaces, quotes, and special chars.
This prevents shell injection and ensures arguments are preserved correctly.
For example: ['python', '-c', 'print("a b")'] should work correctly.
"""
provider = BatchSandboxProvider(mock_k8s_client)
result = provider._build_task_template(
entrypoint=["python", "-c", 'print("hello world")'],
env={"KEY": "value with spaces", "QUOTE": "it's fine"},
)
command = result["spec"]["process"]["command"][2]
# Verify entrypoint args are properly escaped
assert "python" in command
assert "-c" in command
# The python code with spaces and quotes should be properly escaped
assert "'print(" in command or '"print(' in command # Escaped
# Verify env is passed through env list, not in command
env_list = result["spec"]["process"]["env"]
assert {"name": "KEY", "value": "value with spaces"} in env_list
assert {"name": "QUOTE", "value": "it's fine"} in env_list
def test_create_workload_poolref_builds_correct_manifest(self, mock_k8s_client):
"""
Test complete pool-based BatchSandbox manifest structure.
Verifies:
- Basic metadata (apiVersion, kind, name, labels)
- Pool-specific fields (poolRef, taskTemplate, expireTime)
- No template field (pool mode doesn't use pod template)
"""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
expires_at = datetime(2025, 12, 31, 10, 0, 0, tzinfo=timezone.utc)
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri=""),
entrypoint=["python", "app.py"],
env={"FOO": "bar"},
resource_limits={},
labels={"test": "label"},
expires_at=expires_at,
execd_image="execd:latest",
extensions={"poolRef": "test-pool"},
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
# Verify basic structure
assert body["apiVersion"] == "sandbox.opensandbox.io/v1alpha1"
assert body["kind"] == "BatchSandbox"
assert body["metadata"]["name"] == "test-id"
assert body["metadata"]["labels"] == {"test": "label"}
# Verify pool-specific fields
assert body["spec"]["replicas"] == 1
assert body["spec"]["poolRef"] == "test-pool"
assert body["spec"]["expireTime"] == "2025-12-31T10:00:00+00:00"
assert "taskTemplate" in body["spec"]
# Verify no template field (pool-based doesn't use template)
assert "template" not in body["spec"]
def test_create_workload_poolref_default_entrypoint_no_env_omits_task_template(
self, mock_k8s_client
):
"""When entrypoint is SDK default and env is empty, taskTemplate is omitted."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="dockurr/windows:latest"),
entrypoint=["tail", "-f", "/dev/null"],
env={},
resource_limits={},
labels={},
expires_at=None,
execd_image="execd:latest",
extensions={"poolRef": "my-pool"},
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
assert body["spec"]["poolRef"] == "my-pool"
assert "taskTemplate" not in body["spec"]
def test_create_workload_poolref_default_entrypoint_with_env_includes_task_template(
self, mock_k8s_client
):
"""When entrypoint is SDK default but env is non-empty, taskTemplate is generated."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="dockurr/windows:latest"),
entrypoint=["tail", "-f", "/dev/null"],
env={"VERSION": "11"},
resource_limits={},
labels={},
expires_at=None,
execd_image="execd:latest",
extensions={"poolRef": "my-pool"},
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
assert body["spec"]["poolRef"] == "my-pool"
assert "taskTemplate" in body["spec"]
task_template = body["spec"]["taskTemplate"]
assert task_template["spec"]["process"]["env"] == [{"name": "VERSION", "value": "11"}]
def test_create_workload_poolref_none_entrypoint_no_env_omits_task_template(self, mock_k8s_client):
"""When entrypoint is None and env is empty, taskTemplate is omitted.
SDK pool mode callers omit entrypoint entirely (None), expecting the pool's
default command to run. This must not raise a TypeError.
"""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=None,
env={},
resource_limits={},
labels={},
expires_at=None,
execd_image="execd:latest",
extensions={"poolRef": "my-pool"},
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
assert body["spec"]["poolRef"] == "my-pool"
assert "taskTemplate" not in body["spec"]
class TestBatchSandboxProviderEgress:
"""BatchSandboxProvider egress sidecar tests"""
def test_create_workload_without_network_policy_no_sidecar(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
expires_at = datetime(2025, 12, 31, 10, 0, 0, tzinfo=timezone.utc)
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=expires_at,
execd_image="execd:latest",
network_policy=None,
egress_image=None,
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
containers = pod_spec["containers"]
# Should only have main container
assert len(containers) == 1
assert containers[0]["name"] == "sandbox"
# Should not have securityContext with sysctls
assert "securityContext" not in pod_spec or "sysctls" not in pod_spec.get(
"securityContext", {}
)
def test_create_workload_with_network_policy_adds_sidecar(self, mock_k8s_client):
provider = BatchSandboxProvider(
mock_k8s_client,
_app_config_with_egress_disable_ipv6(),
)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
expires_at = datetime(2025, 12, 31, 10, 0, 0, tzinfo=timezone.utc)
network_policy = NetworkPolicy(
default_action="deny",
egress=[NetworkRule(action="allow", target="pypi.org")],
)
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=expires_at,
execd_image="execd:latest",
network_policy=network_policy,
egress_image="opensandbox/egress:v1.1.4",
credential_proxy_enabled=True,
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
containers = pod_spec["containers"]
# Should have both main container and sidecar
assert len(containers) == 2
# Find sidecar container
sidecar = next((c for c in containers if c["name"] == "egress"), None)
assert sidecar is not None
assert sidecar["image"] == "opensandbox/egress:v1.1.4"
# Verify sidecar has environment variable
env_vars = {e["name"]: e["value"] for e in sidecar.get("env", [])}
assert "OPENSANDBOX_EGRESS_RULES" in env_vars
assert env_vars["OPENSANDBOX_EGRESS_MODE"] == EGRESS_MODE_DNS
assert env_vars[OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT] == "true"
caps = sidecar.get("securityContext", {}).get("capabilities", {})
assert "NET_ADMIN" in caps.get("add", [])
assert sidecar.get("securityContext", {}).get("privileged") is not True
assert "command" not in sidecar
assert sidecar["readinessProbe"]["httpGet"]["path"] == "/healthz"
assert sidecar["readinessProbe"]["httpGet"]["port"] == 18080
inits = pod_spec.get("initContainers", [])
assert len(inits) == 1
execd_init = inits[0]
assert execd_init["name"] == "execd-installer"
assert execd_init["image"] == "execd:latest"
assert execd_init.get("securityContext", {}).get("privileged") is True
assert "/proc/sys/net/ipv6/conf/all/disable_ipv6" in execd_init["args"][0]
main = next(c for c in containers if c["name"] == "sandbox")
main_env = {e["name"]: e["value"] for e in main["env"]}
assert main_env[OPENSANDBOX_EGRESS_MITMPROXY_TRANSPARENT] == "true"
assert "SSL_CERT_FILE" not in main_env
assert "REQUESTS_CA_BUNDLE" not in main_env
assert "CURL_CA_BUNDLE" not in main_env
assert "GIT_SSL_CAINFO" not in main_env
assert "NODE_EXTRA_CA_CERTS" not in main_env
assert "opensandbox-mitm-ca" not in {v["name"] for v in pod_spec["volumes"]}
assert {
"name": OPENSANDBOX_RUNTIME_VOLUME_NAME,
"mountPath": OPENSANDBOX_RUNTIME_MOUNT_PATH,
} in main["volumeMounts"]
assert {
"name": OPENSANDBOX_RUNTIME_VOLUME_NAME,
"mountPath": OPENSANDBOX_RUNTIME_MOUNT_PATH,
} in sidecar["volumeMounts"]
def test_create_workload_windows_profile_with_network_policy_keeps_ipv6_disable(
self, mock_k8s_client
):
provider = BatchSandboxProvider(
mock_k8s_client,
_app_config_with_egress_disable_ipv6(),
)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="dockurr/windows:latest"),
entrypoint=["cmd", "/c", "echo hello"],
env={"VERSION": "11"},
resource_limits={"cpu": "4", "memory": "8G", "disk": "64G"},
labels={},
expires_at=None,
execd_image="execd:latest",
platform=PlatformSpec(os="windows", arch="amd64"),
network_policy=NetworkPolicy(default_action="deny", egress=[]),
egress_image="opensandbox/egress:v1.1.4",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
sidecar = next((c for c in pod_spec["containers"] if c["name"] == "egress"), None)
assert sidecar is not None
main_container = next((c for c in pod_spec["containers"] if c["name"] == "sandbox"), None)
assert main_container is not None
main_caps = main_container.get("securityContext", {}).get("capabilities", {})
assert "NET_ADMIN" in main_caps.get("add", [])
assert "NET_RAW" in main_caps.get("add", [])
assert "NET_ADMIN" not in main_caps.get("drop", [])
execd_init = pod_spec["initContainers"][0]
assert execd_init.get("securityContext", {}).get("privileged") is True
assert "/proc/sys/net/ipv6/conf/all/disable_ipv6" in execd_init["args"][0]
def test_create_workload_with_network_policy_persists_annotation_and_sidecar_token(
self, mock_k8s_client
):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=None,
execd_image="execd:latest",
network_policy=NetworkPolicy(default_action="deny", egress=[]),
egress_image="opensandbox/egress:v1.1.4",
annotations={SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY: "egress-token"},
egress_auth_token="egress-token",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
assert (
body["metadata"]["annotations"][SANDBOX_EGRESS_AUTH_TOKEN_METADATA_KEY]
== "egress-token"
)
containers = body["spec"]["template"]["spec"]["containers"]
sidecar = next((c for c in containers if c["name"] == "egress"), None)
assert sidecar is not None
env_vars = {e["name"]: e["value"] for e in sidecar.get("env", [])}
assert env_vars[OPENSANDBOX_EGRESS_TOKEN] == "egress-token"
assert env_vars["OPENSANDBOX_EGRESS_MODE"] == EGRESS_MODE_DNS
assert sidecar["readinessProbe"]["httpGet"]["httpHeaders"] == [
{"name": OPEN_SANDBOX_EGRESS_AUTH_HEADER, "value": "egress-token"}
]
def test_create_workload_with_egress_mode_dns_nft(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=None,
execd_image="execd:latest",
network_policy=NetworkPolicy(default_action="deny", egress=[]),
egress_image="opensandbox/egress:v1.1.4",
egress_mode=EGRESS_MODE_DNS_NFT,
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
containers = body["spec"]["template"]["spec"]["containers"]
sidecar = next((c for c in containers if c["name"] == "egress"), None)
assert sidecar is not None
env_vars = {e["name"]: e["value"] for e in sidecar.get("env", [])}
assert env_vars["OPENSANDBOX_EGRESS_MODE"] == EGRESS_MODE_DNS_NFT
def test_create_workload_with_network_policy_does_not_add_pod_ipv6_sysctls(
self, mock_k8s_client
):
"""IPv6 all.disable is applied in privileged execd init, not Pod sysctls."""
provider = BatchSandboxProvider(
mock_k8s_client,
_app_config_with_egress_disable_ipv6(),
)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
expires_at = datetime(2025, 12, 31, 10, 0, 0, tzinfo=timezone.utc)
network_policy = NetworkPolicy(
default_action="deny",
egress=[NetworkRule(action="allow", target="example.com")],
)
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=expires_at,
execd_image="execd:latest",
network_policy=network_policy,
egress_image="opensandbox/egress:v1.1.4",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
assert "securityContext" not in pod_spec or "sysctls" not in pod_spec.get(
"securityContext", {}
)
sidecar = next(c for c in pod_spec["containers"] if c["name"] == "egress")
assert "command" not in sidecar
execd_init = pod_spec["initContainers"][0]
assert execd_init["name"] == "execd-installer"
assert "/proc/sys/net/ipv6/conf/all/disable_ipv6" in execd_init["args"][0]
def test_create_workload_with_egress_skips_ipv6_disable_when_not_configured(
self, mock_k8s_client
):
"""With ``egress.disable_ipv6`` false, execd init is not privileged and does not write disable_ipv6."""
provider = BatchSandboxProvider(
mock_k8s_client,
_app_config_with_egress_disable_ipv6(False),
)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
network_policy = NetworkPolicy(
default_action="deny",
egress=[NetworkRule(action="allow", target="example.com")],
)
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=None,
execd_image="execd:latest",
network_policy=network_policy,
egress_image="opensandbox/egress:v1.1.4",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
execd_init = pod_spec["initContainers"][0]
assert execd_init["name"] == "execd-installer"
assert "securityContext" not in execd_init
assert "/proc/sys/net/ipv6/conf/all/disable_ipv6" not in execd_init["args"][0]
def test_create_workload_with_network_policy_drops_net_admin_from_main_container(
self, mock_k8s_client
):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
expires_at = datetime(2025, 12, 31, 10, 0, 0, tzinfo=timezone.utc)
network_policy = NetworkPolicy(
default_action="deny",
egress=[NetworkRule(action="allow", target="example.com")],
)
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=expires_at,
execd_image="execd:latest",
network_policy=network_policy,
egress_image="opensandbox/egress:v1.1.4",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
containers = pod_spec["containers"]
# Find main container
main_container = next((c for c in containers if c["name"] == "sandbox"), None)
assert main_container is not None
# Verify main container has securityContext
assert "securityContext" in main_container
assert "capabilities" in main_container["securityContext"]
assert "drop" in main_container["securityContext"]["capabilities"]
assert "NET_ADMIN" in main_container["securityContext"]["capabilities"]["drop"]
def test_create_workload_without_egress_image_no_sidecar(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
expires_at = datetime(2025, 12, 31, 10, 0, 0, tzinfo=timezone.utc)
network_policy = NetworkPolicy(
default_action="deny",
egress=[NetworkRule(action="allow", target="example.com")],
)
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=expires_at,
execd_image="execd:latest",
network_policy=network_policy,
egress_image=None,
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
containers = pod_spec["containers"]
# Should only have main container
assert len(containers) == 1
assert containers[0]["name"] == "sandbox"
def test_egress_sidecar_contains_network_policy_in_env(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
expires_at = datetime(2025, 12, 31, 10, 0, 0, tzinfo=timezone.utc)
network_policy = NetworkPolicy(
default_action="deny",
egress=[
NetworkRule(action="allow", target="pypi.org"),
NetworkRule(action="deny", target="*.malicious.com"),
],
)
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=expires_at,
execd_image="execd:latest",
network_policy=network_policy,
egress_image="opensandbox/egress:v1.1.4",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
containers = pod_spec["containers"]
sidecar = next((c for c in containers if c["name"] == "egress"), None)
assert sidecar is not None
env_vars = {e["name"]: e["value"] for e in sidecar.get("env", [])}
assert "OPENSANDBOX_EGRESS_RULES" in env_vars
# Verify the environment variable contains valid JSON with network policy
import json
policy_json = json.loads(env_vars["OPENSANDBOX_EGRESS_RULES"])
assert policy_json["defaultAction"] == "deny"
assert len(policy_json["egress"]) == 2
assert policy_json["egress"][0]["action"] == "allow"
assert policy_json["egress"][0]["target"] == "pypi.org"
def test_main_container_no_security_context_without_network_policy(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
expires_at = datetime(2025, 12, 31, 10, 0, 0, tzinfo=timezone.utc)
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=expires_at,
execd_image="execd:latest",
network_policy=None,
egress_image=None,
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
containers = pod_spec["containers"]
main_container = containers[0]
# Main container should not have securityContext when no network policy
assert "securityContext" not in main_container
def test_create_workload_with_network_policy_works_with_template(
self, mock_k8s_client, tmp_path
):
template_file = tmp_path / "template.yaml"
template_file.write_text(
"""
spec:
template:
spec:
volumes:
- name: sandbox-shared-data
emptyDir: {}
"""
)
provider = BatchSandboxProvider(
mock_k8s_client, _app_config_with_template(str(template_file))
)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
expires_at = datetime(2025, 12, 31, 10, 0, 0, tzinfo=timezone.utc)
network_policy = NetworkPolicy(
default_action="deny",
egress=[NetworkRule(action="allow", target="example.com")],
)
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=expires_at,
execd_image="execd:latest",
network_policy=network_policy,
egress_image="opensandbox/egress:v1.1.4",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
containers = pod_spec["containers"]
# Should have both main container and sidecar
assert len(containers) == 2
# Verify sidecar exists
sidecar = next((c for c in containers if c["name"] == "egress"), None)
assert sidecar is not None
# Pod-level IPv6 sysctls are not injected for egress (sidecar startup handles all.disable)
assert "securityContext" not in pod_spec or "sysctls" not in pod_spec.get(
"securityContext", {}
)
# Verify template volumes are still merged
volume_names = [v["name"] for v in pod_spec["volumes"]]
assert "sandbox-shared-data" in volume_names
assert "opensandbox-bin" in volume_names
# ===== Phase + Condition Validation Tests =====
def test_pause_sandbox_running_allows(self, mock_k8s_client):
"""Test pause allowed when Phase=Succeed."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = {
"metadata": {"name": "test-id", "namespace": "test-ns"},
"status": {"phase": "Succeed", "conditions": []},
}
mock_k8s_client.patch_custom_object.return_value = {}
provider.pause_sandbox("test-id", "test-ns")
mock_k8s_client.patch_custom_object.assert_called_once()
call_kwargs = mock_k8s_client.patch_custom_object.call_args.kwargs
assert call_kwargs["body"] == {"spec": {"pause": True}}
def test_pause_sandbox_running_with_pause_failed_allows_retry(self, mock_k8s_client):
"""Test pause retry performs an internal nil->true double patch."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = {
"metadata": {"name": "test-id", "namespace": "test-ns"},
"status": {
"phase": "Succeed",
"conditions": [
{"type": "PauseFailed", "status": "True", "reason": "SnapshotFailed"}
],
},
}
mock_k8s_client.patch_custom_object.return_value = {}
provider.pause_sandbox("test-id", "test-ns")
assert mock_k8s_client.patch_custom_object.call_count == 2
first_patch = mock_k8s_client.patch_custom_object.call_args_list[0].kwargs["body"]
second_patch = mock_k8s_client.patch_custom_object.call_args_list[1].kwargs["body"]
assert first_patch == {"spec": {"pause": None}}
assert second_patch == {"spec": {"pause": True}}
def test_patch_pause_with_retry_bridge_accepts_second_patch_timeout_when_readback_matches_target(
self, mock_k8s_client
):
provider = BatchSandboxProvider(mock_k8s_client)
provider.patch_workload = MagicMock(
side_effect=[{}, ApiException(status=500, reason="timeout")]
)
provider.get_workload = MagicMock(
return_value={
"metadata": {"name": "test-id", "namespace": "test-ns"},
"spec": {"pause": True},
}
)
provider._patch_pause_with_retry_bridge("test-id", "test-ns", True)
assert provider.patch_workload.call_count == 2
first_call = provider.patch_workload.call_args_list[0].args
second_call = provider.patch_workload.call_args_list[1].args
assert first_call == ("test-id", "test-ns", {"spec": {"pause": None}})
assert second_call == ("test-id", "test-ns", {"spec": {"pause": True}})
provider.get_workload.assert_called_once_with("test-id", "test-ns")
def test_patch_pause_with_retry_bridge_retries_target_when_readback_still_nil(
self, mock_k8s_client
):
provider = BatchSandboxProvider(mock_k8s_client)
provider.patch_workload = MagicMock(
side_effect=[{}, ApiException(status=500, reason="timeout"), {}]
)
provider.get_workload = MagicMock(
return_value={
"metadata": {"name": "test-id", "namespace": "test-ns"},
"spec": {"pause": None},
}
)
provider._patch_pause_with_retry_bridge("test-id", "test-ns", True)
assert provider.patch_workload.call_count == 3
first_call = provider.patch_workload.call_args_list[0].args
second_call = provider.patch_workload.call_args_list[1].args
third_call = provider.patch_workload.call_args_list[2].args
assert first_call == ("test-id", "test-ns", {"spec": {"pause": None}})
assert second_call == ("test-id", "test-ns", {"spec": {"pause": True}})
assert third_call == ("test-id", "test-ns", {"spec": {"pause": True}})
provider.get_workload.assert_called_once_with("test-id", "test-ns")
def test_pause_sandbox_pausing_rejects(self, mock_k8s_client):
"""Test pause rejected when Phase=Pausing."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = {
"metadata": {"name": "test-id", "namespace": "test-ns"},
"status": {"phase": "Pausing", "conditions": []},
}
with pytest.raises(ValueError, match="operation in progress"):
provider.pause_sandbox("test-id", "test-ns")
def test_pause_sandbox_resuming_rejects(self, mock_k8s_client):
"""Test pause rejected when Phase=Resuming."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = {
"metadata": {"name": "test-id", "namespace": "test-ns"},
"status": {"phase": "Resuming", "conditions": []},
}
with pytest.raises(ValueError, match="operation in progress"):
provider.pause_sandbox("test-id", "test-ns")
def test_pause_sandbox_paused_rejects(self, mock_k8s_client):
"""Test pause rejected when Phase=Paused."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = {
"metadata": {"name": "test-id", "namespace": "test-ns"},
"status": {"phase": "Paused", "conditions": []},
}
with pytest.raises(ValueError, match="already paused"):
provider.pause_sandbox("test-id", "test-ns")
def test_pause_sandbox_failed_rejects(self, mock_k8s_client):
"""Test pause rejected when Phase=Failed."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = {
"metadata": {"name": "test-id", "namespace": "test-ns"},
"status": {"phase": "Failed", "conditions": []},
}
with pytest.raises(ValueError, match="not available"):
provider.pause_sandbox("test-id", "test-ns")
def test_pause_sandbox_failed_with_pause_failed_rejects(self, mock_k8s_client):
"""Test pause rejected when Phase=Failed + PauseFailed=True (pod loss scenario)."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = {
"metadata": {"name": "test-id", "namespace": "test-ns"},
"status": {
"phase": "Failed",
"conditions": [{"type": "PauseFailed", "status": "True", "reason": "PodNotFound"}],
},
}
with pytest.raises(ValueError, match="pause caused pod loss"):
provider.pause_sandbox("test-id", "test-ns")
def test_pause_sandbox_pending_rejects(self, mock_k8s_client):
"""Test pause rejected when Phase=Pending."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = {
"metadata": {"name": "test-id", "namespace": "test-ns"},
"status": {"phase": "Pending", "conditions": []},
}
with pytest.raises(ValueError, match="being created"):
provider.pause_sandbox("test-id", "test-ns")
def test_resume_sandbox_paused_allows(self, mock_k8s_client):
"""Test resume allowed when Phase=Paused."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = {
"metadata": {"name": "test-id", "namespace": "test-ns"},
"status": {"phase": "Paused", "conditions": []},
}
mock_k8s_client.patch_custom_object.return_value = {}
provider.resume_sandbox("test-id", "test-ns")
mock_k8s_client.patch_custom_object.assert_called_once()
call_kwargs = mock_k8s_client.patch_custom_object.call_args.kwargs
assert call_kwargs["body"] == {"spec": {"pause": False}}
def test_resume_sandbox_paused_with_resume_failed_allows_retry(self, mock_k8s_client):
"""Test resume retry performs an internal nil->false double patch."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = {
"metadata": {"name": "test-id", "namespace": "test-ns"},
"status": {
"phase": "Paused",
"conditions": [
{"type": "ResumeFailed", "status": "True", "reason": "SnapshotNotReady"}
],
},
}
mock_k8s_client.patch_custom_object.return_value = {}
provider.resume_sandbox("test-id", "test-ns")
assert mock_k8s_client.patch_custom_object.call_count == 2
first_patch = mock_k8s_client.patch_custom_object.call_args_list[0].kwargs["body"]
second_patch = mock_k8s_client.patch_custom_object.call_args_list[1].kwargs["body"]
assert first_patch == {"spec": {"pause": None}}
assert second_patch == {"spec": {"pause": False}}
def test_resume_sandbox_resuming_rejects(self, mock_k8s_client):
"""Test resume rejected when Phase=Resuming."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = {
"metadata": {"name": "test-id", "namespace": "test-ns"},
"status": {"phase": "Resuming", "conditions": []},
}
with pytest.raises(ValueError, match="operation in progress"):
provider.resume_sandbox("test-id", "test-ns")
def test_resume_sandbox_pausing_rejects(self, mock_k8s_client):
"""Test resume rejected when Phase=Pausing."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = {
"metadata": {"name": "test-id", "namespace": "test-ns"},
"status": {"phase": "Pausing", "conditions": []},
}
with pytest.raises(ValueError, match="operation in progress"):
provider.resume_sandbox("test-id", "test-ns")
def test_resume_sandbox_running_rejects(self, mock_k8s_client):
"""Test resume rejected when Phase=Succeed."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = {
"metadata": {"name": "test-id", "namespace": "test-ns"},
"status": {"phase": "Succeed", "conditions": []},
}
with pytest.raises(ValueError, match="expected Paused"):
provider.resume_sandbox("test-id", "test-ns")
def test_get_status_succeed_phase_maps_to_running_state(self):
provider = BatchSandboxProvider(MagicMock())
workload = {
"status": {"phase": "Succeed"},
"metadata": {"creationTimestamp": "2025-12-24T10:00:00Z"},
}
result = provider.get_status(workload)
assert result["state"] == "Running"
assert result["reason"] == "RUNNING"
assert result["message"] == "Sandbox is running"
def test_get_status_failed_uses_condition_message(self):
provider = BatchSandboxProvider(MagicMock())
workload = {
"status": {
"phase": "Failed",
"conditions": [
{
"type": "ResumeFailed",
"status": "True",
"reason": "PodStartFailed",
"message": "Pod sandbox-abc-0: ImagePullBackOff - image not found",
}
],
},
"metadata": {"creationTimestamp": "2025-12-24T10:00:00Z"},
}
result = provider.get_status(workload)
assert result["state"] == "Failed"
assert result["reason"] == "FAILED"
assert result["message"] == "Pod sandbox-abc-0: ImagePullBackOff - image not found"
def test_resume_sandbox_failed_rejects(self, mock_k8s_client):
"""Test resume rejected when Phase=Failed."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = {
"metadata": {"name": "test-id", "namespace": "test-ns"},
"status": {"phase": "Failed", "conditions": []},
}
with pytest.raises(ValueError, match="not available"):
provider.resume_sandbox("test-id", "test-ns")
def test_resume_sandbox_failed_with_resume_failed_rejects(self, mock_k8s_client):
"""Test resume rejected when Phase=Failed + ResumeFailed=True (pod start failure)."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = {
"metadata": {"name": "test-id", "namespace": "test-ns"},
"status": {
"phase": "Failed",
"conditions": [
{"type": "ResumeFailed", "status": "True", "reason": "PodStartFailed"}
],
},
}
with pytest.raises(ValueError, match="resume caused pod start failure"):
provider.resume_sandbox("test-id", "test-ns")
def test_resume_sandbox_pending_rejects(self, mock_k8s_client):
"""Test resume rejected when Phase=Pending."""
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.get_custom_object.return_value = {
"metadata": {"name": "test-id", "namespace": "test-ns"},
"status": {"phase": "Pending", "conditions": []},
}
with pytest.raises(ValueError, match="being created"):
provider.resume_sandbox("test-id", "test-ns")
# ===== Image Auth Tests =====
def test_supports_image_auth_returns_true(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
assert provider.supports_image_auth() is True
def test_create_workload_with_image_auth_injects_image_pull_secrets(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "uid-123"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(
uri="registry.example.com/img:tag",
auth=ImageAuth(username="user", password="pass"),
),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pull_secrets = body["spec"]["template"]["spec"].get("imagePullSecrets")
assert pull_secrets == [{"name": f"{IMAGE_AUTH_SECRET_PREFIX}-test-id"}]
def test_create_workload_with_image_auth_creates_secret(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "uid-abc"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(
uri="registry.example.com/img:tag",
auth=ImageAuth(username="user", password="pass"),
),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
)
mock_k8s_client.create_secret.assert_called_once()
call_kwargs = mock_k8s_client.create_secret.call_args.kwargs
assert call_kwargs["namespace"] == "test-ns"
secret = call_kwargs["body"]
assert secret.type == "kubernetes.io/dockerconfigjson"
ref = secret.metadata.owner_references[0]
assert ref.uid == "uid-abc"
assert ref.kind == "BatchSandbox"
assert ref.name == "test-id"
def test_create_workload_without_image_auth_skips_secret(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "uid-123"}
}
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
)
mock_k8s_client.create_secret.assert_not_called()
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
assert "imagePullSecrets" not in body["spec"]["template"]["spec"]
def test_create_workload_with_image_auth_secret_failure_rolls_back_batchsandbox(
self, mock_k8s_client
):
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "uid-123"}
}
mock_k8s_client.create_secret.side_effect = ApiException(status=403)
with pytest.raises(ApiException):
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(
uri="registry.example.com/img:tag",
auth=ImageAuth(username="user", password="pass"),
),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
)
mock_k8s_client.delete_custom_object.assert_called_once_with(
group=provider.group,
version=provider.version,
namespace="test-ns",
plural=provider.plural,
name="test-id",
grace_period_seconds=0,
)
# ===== Volume Support Tests =====
def test_create_workload_with_pvc_volume(self, mock_k8s_client):
"""
Test creating workload with PVC volume mount.
Verifies:
- PVC volume is correctly added to pod spec
- Volume mount is added to main container
- claimName is correctly set
"""
from opensandbox_server.api.schema import Volume, PVC
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
expires_at = datetime(2025, 12, 31, 10, 0, 0, tzinfo=timezone.utc)
volumes = [
Volume(
name="data-volume",
pvc=PVC(claim_name="my-pvc"),
mount_path="/mnt/data",
read_only=False,
)
]
result = provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=expires_at,
execd_image="execd:latest",
volumes=volumes,
)
assert result == {"name": "test-id", "uid": "test-uid", "apiVersion": "sandbox.opensandbox.io/v1alpha1", "kind": "BatchSandbox"}
def test_create_workload_poolref_rejects_platform(self, mock_k8s_client):
provider = BatchSandboxProvider(mock_k8s_client)
with pytest.raises(
ValueError, match="platform is not supported together with extensions.poolRef"
):
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={"opensandbox.io/id": "test-id"},
expires_at=None,
execd_image="execd:latest",
extensions={"poolRef": "warm-pool"},
platform=PlatformSpec(os="linux", arch="amd64"),
)
def test_create_workload_with_pvc_volume_readonly(self, mock_k8s_client):
"""
Test creating workload with read-only PVC volume mount.
"""
from opensandbox_server.api.schema import Volume, PVC
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
volumes = [
Volume(
name="models-volume",
pvc=PVC(claim_name="models-pvc"),
mount_path="/mnt/models",
read_only=True,
)
]
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
volumes=volumes,
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
main_container = pod_spec["containers"][0]
mounts = main_container.get("volumeMounts", [])
models_mount = next((m for m in mounts if m["name"] == "models-volume"), None)
models_volume = next((v for v in pod_spec["volumes"] if v["name"] == "models-volume"), None)
assert models_mount is not None
assert models_mount["readOnly"] is True
assert models_volume is not None
assert models_volume["persistentVolumeClaim"]["readOnly"] is True
def test_create_workload_with_pvc_volume_subpath(self, mock_k8s_client):
"""
Test creating workload with PVC volume mount with subPath.
"""
from opensandbox_server.api.schema import Volume, PVC
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
volumes = [
Volume(
name="data-volume",
pvc=PVC(claim_name="shared-pvc"),
mount_path="/mnt/data",
sub_path="task-001",
read_only=False,
)
]
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
volumes=volumes,
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
main_container = pod_spec["containers"][0]
mounts = main_container.get("volumeMounts", [])
data_mount = next((m for m in mounts if m["name"] == "data-volume"), None)
assert data_mount is not None
assert data_mount.get("subPath") == "task-001"
def test_create_workload_with_host_volume(self, mock_k8s_client):
"""
Test creating workload with hostPath volume mount.
"""
from opensandbox_server.api.schema import Volume, Host
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
volumes = [
Volume(
name="host-volume",
host=Host(path="/data/shared"),
mount_path="/mnt/host",
read_only=True,
)
]
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
volumes=volumes,
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
# Check volume definition
volumes_list = pod_spec.get("volumes", [])
host_volume = next((v for v in volumes_list if v["name"] == "host-volume"), None)
assert host_volume is not None
assert host_volume["hostPath"]["path"] == "/data/shared"
assert host_volume["hostPath"]["type"] == "DirectoryOrCreate"
# Check volume mount
main_container = pod_spec["containers"][0]
mounts = main_container.get("volumeMounts", [])
host_mount = next((m for m in mounts if m["name"] == "host-volume"), None)
assert host_mount is not None
assert host_mount["mountPath"] == "/mnt/host"
assert host_mount["readOnly"] is True
def test_create_workload_with_multiple_volumes(self, mock_k8s_client):
"""
Test creating workload with multiple volumes (PVC and hostPath).
"""
from opensandbox_server.api.schema import Volume, PVC, Host
provider = BatchSandboxProvider(mock_k8s_client)
mock_k8s_client.create_custom_object.return_value = {
"metadata": {"name": "test-id", "uid": "test-uid"}
}
volumes = [
Volume(
name="pvc-volume",
pvc=PVC(claim_name="data-pvc"),
mount_path="/mnt/data",
read_only=False,
),
Volume(
name="host-volume",
host=Host(path="/tmp/cache"),
mount_path="/mnt/cache",
read_only=True,
),
]
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
volumes=volumes,
)
body = mock_k8s_client.create_custom_object.call_args.kwargs["body"]
pod_spec = body["spec"]["template"]["spec"]
# Check both volumes exist
volumes_list = pod_spec.get("volumes", [])
assert len([v for v in volumes_list if v["name"] in ("pvc-volume", "host-volume")]) == 2
# Check both mounts exist
main_container = pod_spec["containers"][0]
mounts = main_container.get("volumeMounts", [])
mount_names = {m["name"] for m in mounts}
assert "pvc-volume" in mount_names
assert "host-volume" in mount_names
def test_create_workload_pool_mode_rejects_volumes(self, mock_k8s_client):
"""
Test that pool mode rejects volumes with clear error message.
"""
from opensandbox_server.api.schema import Volume, PVC
provider = BatchSandboxProvider(mock_k8s_client)
volumes = [
Volume(
name="data-volume",
pvc=PVC(claim_name="my-pvc"),
mount_path="/mnt/data",
)
]
with pytest.raises(ValueError, match="Pool mode does not support volumes"):
provider.create_workload(
sandbox_id="test-id",
namespace="test-ns",
image_spec=ImageSpec(uri="python:3.11"),
entrypoint=["/bin/bash"],
env={},
resource_limits={},
labels={},
expires_at=datetime(2025, 12, 31, tzinfo=timezone.utc),
execd_image="execd:latest",
extensions={"poolRef": "my-pool"},
volumes=volumes,
)
def test_apply_volumes_to_pod_spec_empty_volumes(self, mock_k8s_client):
"""
Test apply_volumes_to_pod_spec with empty volumes list.
"""
pod_spec = {
"containers": [{"name": "main", "volumeMounts": []}],
"volumes": [],
}
apply_volumes_to_pod_spec(pod_spec, [])
# Should not modify pod_spec
assert pod_spec["volumes"] == []
assert pod_spec["containers"][0]["volumeMounts"] == []
def test_apply_volumes_to_pod_spec_no_containers(self, mock_k8s_client):
"""
Test apply_volumes_to_pod_spec with no containers returns early without error.
"""
from opensandbox_server.api.schema import Volume, PVC
pod_spec = {"volumes": []}
volumes = [Volume(name="test", pvc=PVC(claim_name="pvc"), mount_path="/mnt")]
# Should not raise exception
apply_volumes_to_pod_spec(pod_spec, volumes)
# Pod spec should remain unchanged (no containers to mount to)
assert pod_spec["volumes"] == []
def test_apply_volumes_to_pod_spec_duplicate_internal_volume(self, mock_k8s_client):
"""
Test apply_volumes_to_pod_spec rejects volume names that collide with internal volumes.
"""
from opensandbox_server.api.schema import Volume, PVC
pod_spec = {
"containers": [{"name": "sandbox", "volumeMounts": []}],
"volumes": [{"name": "opensandbox-bin", "emptyDir": {}}],
}
volumes = [Volume(name="opensandbox-bin", pvc=PVC(claim_name="pvc"), mount_path="/mnt")]
# Should raise ValueError for duplicate volume name
with pytest.raises(ValueError) as exc_info:
apply_volumes_to_pod_spec(pod_spec, volumes)
assert "conflicts with an internal volume" in str(exc_info.value)
def test_apply_volumes_to_pod_spec_same_pvc_multiple_mounts(self, mock_k8s_client):
"""
When multiple Volume API objects share the same claim_name, only one
Kubernetes volume is created; multiple volumeMounts reference it (avoids
CSI driver issues from duplicate PVC volume definitions).
"""
from opensandbox_server.api.schema import Volume, PVC
pod_spec = {
"containers": [{"name": "main", "volumeMounts": []}],
"volumes": [],
}
volumes = [
Volume(
name="skills",
pvc=PVC(claim_name="oss-pvc-r"),
mount_path="/path/to/skills",
sub_path="skill-hub/publish",
read_only=True,
),
Volume(
name="draft",
pvc=PVC(claim_name="oss-pvc-r"),
mount_path="/path/to/draft",
sub_path="skill-hub/draft",
read_only=True,
),
]
apply_volumes_to_pod_spec(pod_spec, volumes)
# One volume definition for the shared PVC (first Volume name used)
assert len(pod_spec["volumes"]) == 1
assert pod_spec["volumes"][0]["name"] == "skills"
shared_volume = next((v for v in pod_spec["volumes"] if v["name"] == "skills"), None)
assert shared_volume is not None
assert shared_volume["persistentVolumeClaim"]["claimName"] == "oss-pvc-r"
assert shared_volume["persistentVolumeClaim"]["readOnly"] is True
# Two volumeMounts, both referencing the same volume name
mounts = pod_spec["containers"][0]["volumeMounts"]
assert len(mounts) == 2
by_path = {m["mountPath"]: m for m in mounts}
assert by_path["/path/to/skills"]["name"] == "skills"
assert by_path["/path/to/skills"].get("subPath") == "skill-hub/publish"
assert by_path["/path/to/draft"]["name"] == "skills"
assert by_path["/path/to/draft"].get("subPath") == "skill-hub/draft"
def test_apply_volumes_to_pod_spec_same_pvc_multiple_mounts_readwrite(self, mock_k8s_client):
"""Shared PVC mounts with read_only=False should keep source-level readOnly false."""
from opensandbox_server.api.schema import Volume, PVC
pod_spec = {
"containers": [{"name": "main", "volumeMounts": []}],
"volumes": [],
}
volumes = [
Volume(
name="skills",
pvc=PVC(claim_name="oss-pvc-rw"),
mount_path="/path/to/skills",
sub_path="skill-hub/publish",
read_only=False,
),
Volume(
name="draft",
pvc=PVC(claim_name="oss-pvc-rw"),
mount_path="/path/to/draft",
sub_path="skill-hub/draft",
read_only=False,
),
]
apply_volumes_to_pod_spec(pod_spec, volumes)
assert len(pod_spec["volumes"]) == 1
shared_volume = next((v for v in pod_spec["volumes"] if v["name"] == "skills"), None)
assert shared_volume is not None
assert shared_volume["persistentVolumeClaim"]["claimName"] == "oss-pvc-rw"
assert shared_volume["persistentVolumeClaim"]["readOnly"] is False
mounts = pod_spec["containers"][0]["volumeMounts"]
assert len(mounts) == 2
assert all(mount["name"] == "skills" for mount in mounts)
assert all(mount["readOnly"] is False for mount in mounts)
def test_apply_volumes_to_pod_spec_same_pvc_mixed_read_only_raises(self, mock_k8s_client):
"""Shared PVC mounts with mixed read_only values should fail fast."""
from opensandbox_server.api.schema import Volume, PVC
pod_spec = {
"containers": [{"name": "main", "volumeMounts": []}],
"volumes": [],
}
volumes = [
Volume(
name="skills",
pvc=PVC(claim_name="oss-pvc-mixed"),
mount_path="/path/to/skills",
read_only=False,
),
Volume(
name="draft",
pvc=PVC(claim_name="oss-pvc-mixed"),
mount_path="/path/to/draft",
read_only=True,
),
]
with pytest.raises(ValueError, match="mixed read_only values"):
apply_volumes_to_pod_spec(pod_spec, volumes)