Files
AstraBox/tests/sandbox_server_launcher_test.py
Colton QiandClaude 5ed0137859 Release 0.1.1
Strengthen sandbox isolation and authentication, make all five engines work
through the bundled installer, and preserve conversations across sandbox and
service restarts. Add team login and single-container deployment, with upgrade
instructions for replacing existing 0.1.0 sandboxes.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-26 19:09:28 -07:00

2936 lines
118 KiB
Python

"""The sandbox-server launcher: configuration refusals and compatibility checks.
Four layers, because they fail for different reasons:
1. **Configuration.** Every value AstraBox renders into the lifecycle server's
TOML, and every shape that must be REFUSED rather than passed through —
upstream's posture on a bad value is a warning and a silent fallback, which
is how a typo becomes a container with no cgroup or a sandbox nobody can
reach.
2. **The compatibility hooks, against stubs.**
``let_docker_assign_published_ports``, ``redirect_proxy_host``,
``redirect_public_endpoint_host`` and ``redirect_metadata_store_root``
reach into upstream internals, so each
is nailed against a stub carrying upstream's shape: the nail fires in the
unit lane WITHOUT the optional extra, and it fires for a rename, for a
collaborator that stops consulting the constant, and for an unwritable
directory.
3. **The same hooks against the REAL modules**, wherever the extra is installed
(skipped otherwise) — a stub can only prove the code is self-consistent; only
the real module can prove the constant is still there, still spelled that way,
and still carrying the wrong-for-us default that makes the rebind necessary.
4. **The Kubernetes runtime**: its document, its refusals, and its startup proof
— the last against stubs carrying the Kubernetes client's shape, so it fires
without a cluster. The Docker document is pinned as RENDERED BYTES in this
layer, because "the second runtime did not move the first one" is the claim
the whole split rests on.
"""
from __future__ import annotations
import importlib
import inspect
import json
import logging
import os
import stat
import sys
import tomllib
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import ModuleType, SimpleNamespace
from typing import Any, Iterable, Iterator
import pytest
from astrabox.deploy import sandbox_server as launcher
_ALL_ENV = (
launcher.SERVER_PORT_ENV,
launcher.METADATA_DIR_ENV,
launcher.NETWORK_MODE_ENV,
launcher.EXECD_IMAGE_ENV,
"ASTRABOX_AGENT_IMAGE",
launcher.PORT_RANGE_ENV,
launcher.PIDS_LIMIT_ENV,
launcher.PUBLISH_HOST_IP_ENV,
launcher.CREDENTIAL_VAULT_ENV,
launcher.EGRESS_DNS_UPSTREAM_ENV,
launcher.LOG_LEVEL_ENV,
launcher.RUNTIME_ENV,
launcher.KUBECONFIG_ENV,
launcher.KUBE_API_SERVER_ENV,
launcher.KUBE_NAMESPACE_ENV,
launcher.KUBE_WORKLOAD_PROVIDER_ENV,
launcher.KUBE_IMAGE_PULL_POLICY_ENV,
launcher.KUBE_INFORMER_ENV,
launcher.INGRESS_MODE_ENV,
launcher.INGRESS_GATEWAY_ADDRESS_ENV,
launcher.INGRESS_ROUTE_MODE_ENV,
launcher.INGRESS_SIGNING_KEY_ENV,
launcher.INGRESS_SIGNING_KEY_ID_ENV,
launcher.SECURE_ACCESS_ENV,
)
@pytest.fixture(autouse=True)
def _clean_env(monkeypatch: pytest.MonkeyPatch) -> None:
"""Every test starts from "nothing configured" so defaults are the defaults."""
for name in _ALL_ENV:
monkeypatch.delenv(name, raising=False)
@pytest.fixture
def state_dir(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
"""Point AstraBox's ``state_dir`` at a tmp path (the metadata-dir default)."""
root = tmp_path / "state"
root.mkdir()
monkeypatch.setattr(
launcher,
"metadata_dir",
lambda: root / launcher.DEFAULT_METADATA_DIRNAME,
)
return root
# ---------------------------------------------------------------------------
# 1. configuration
# ---------------------------------------------------------------------------
def test_server_port_defaults_and_composes_the_base_url() -> None:
assert launcher.server_port() == int(launcher.DEFAULT_SERVER_PORT)
assert launcher.lifecycle_base_url() == (
f"http://{launcher.SERVER_BIND_HOST}:{launcher.DEFAULT_SERVER_PORT}"
)
def test_server_port_is_loopback_only_and_never_configurable() -> None:
# The bind address is a constant, not a knob: the API has no auth and can
# create privileged containers on the host daemon.
assert launcher.SERVER_BIND_HOST == "127.0.0.1"
assert launcher.lifecycle_base_url().startswith("http://127.0.0.1:")
@pytest.mark.parametrize("value", ["", "0", "70000", "-1", "http://x", "8990.5"])
def test_server_port_refuses_a_non_port(monkeypatch: pytest.MonkeyPatch, value: str) -> None:
monkeypatch.setenv(launcher.SERVER_PORT_ENV, value or "not-a-port")
with pytest.raises(launcher.SandboxServerConfigError, match=launcher.SERVER_PORT_ENV):
launcher.server_port()
def test_network_mode_defaults_to_bridge() -> None:
assert launcher.network_mode() == "bridge"
@pytest.mark.parametrize("value", ["host", "HOST", " host ", "container:abc123", "Container:x"])
def test_network_mode_refuses_a_shared_network_namespace(
monkeypatch: pytest.MonkeyPatch, value: str
) -> None:
# Every sandbox binds the SAME fixed in-box ports, and a host-networked
# sandbox's endpoint carries no sandbox identity, so a shared netns makes one
# session's data plane land in another session's box.
monkeypatch.setenv(launcher.NETWORK_MODE_ENV, value)
with pytest.raises(launcher.SandboxServerConfigError, match="shared network namespace"):
launcher.network_mode()
def test_network_mode_refuses_none(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv(launcher.NETWORK_MODE_ENV, "none")
with pytest.raises(launcher.SandboxServerConfigError, match="publishes no"):
launcher.network_mode()
def test_default_vault_refuses_a_user_defined_network(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv(launcher.NETWORK_MODE_ENV, "astrabox-sandboxes")
with pytest.raises(launcher.SandboxServerConfigError) as error:
launcher.network_mode()
assert "Credential Vault" in str(error.value)
assert "bridge" in str(error.value)
def test_user_defined_network_remains_available_when_vault_is_off(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv(launcher.CREDENTIAL_VAULT_ENV, "false")
monkeypatch.setenv(launcher.NETWORK_MODE_ENV, "astrabox-sandboxes")
assert launcher.network_mode() == "astrabox-sandboxes"
def test_the_bundled_server_allows_only_the_host_owned_dns_upstream() -> None:
helpers = ModuleType("opensandbox_server.services.helpers")
helpers.ALLOWED_EGRESS_ENV_VARS = frozenset({"OPENSANDBOX_EGRESS_LOG_LEVEL"})
def split_egress_env(env: dict[str, str]) -> tuple[dict[str, str], dict[str, str]]:
allowed = helpers.ALLOWED_EGRESS_ENV_VARS
for key in env:
if key.startswith("OPENSANDBOX_EGRESS_") and key not in allowed:
raise ValueError(key)
return (
{key: value for key, value in env.items() if not key.startswith("OPENSANDBOX_EGRESS_")},
{key: value for key, value in env.items() if key.startswith("OPENSANDBOX_EGRESS_")},
)
helpers.split_egress_env = split_egress_env # type: ignore[attr-defined]
launcher.allow_host_controlled_egress_dns_upstream(helpers)
sandbox_env, egress_env = helpers.split_egress_env( # type: ignore[attr-defined]
{"USER_ENV": "kept", launcher.UPSTREAM_DNS_ENV: "172.17.0.1:5353"}
)
assert sandbox_env == {"USER_ENV": "kept"}
assert egress_env == {launcher.UPSTREAM_DNS_ENV: "172.17.0.1:5353"}
def _kernel_ephemeral_range(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, low: int, high: int
) -> None:
"""Pin what the host says its outgoing source-port range is."""
proc = tmp_path / "ip_local_port_range"
proc.write_text(f"{low}\t{high}\n")
monkeypatch.setattr(launcher, "EPHEMERAL_PORT_RANGE_FILE", str(proc))
def test_port_range_defaults_below_the_kernels_ephemeral_range(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
_kernel_ephemeral_range(monkeypatch, tmp_path, 32768, 60999)
assert launcher.port_range() == (20000, 32000)
def test_port_range_refuses_a_span_the_kernel_also_hands_out(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""The span this deployment shipped before, against a stock Linux host.
Published sandbox ports and outgoing connections came from one pool, so
`docker start` lost the race whenever enough sandboxes were created at
once: "failed to bind host port 0.0.0.0:42838/tcp: address already in
use", on a port no container held.
"""
_kernel_ephemeral_range(monkeypatch, tmp_path, 32768, 60999)
monkeypatch.setenv(launcher.PORT_RANGE_ENV, "40000-60000")
with pytest.raises(launcher.SandboxServerConfigError, match=launcher.PORT_RANGE_ENV):
launcher.port_range()
@pytest.mark.parametrize(
"value",
[
"32000-40000", # starts below, runs into it
"50000-61000", # starts inside, runs past the end
"20000-65000", # contains it
"40000-41000", # wholly inside
],
)
def test_port_range_refuses_every_kind_of_overlap(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, value: str
) -> None:
_kernel_ephemeral_range(monkeypatch, tmp_path, 32768, 60999)
monkeypatch.setenv(launcher.PORT_RANGE_ENV, value)
with pytest.raises(launcher.SandboxServerConfigError, match="ephemeral port"):
launcher.port_range()
def test_port_range_accepts_a_span_above_a_narrowed_kernel_range(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""A host may move its own range; only the actual overlap is refused."""
_kernel_ephemeral_range(monkeypatch, tmp_path, 10000, 20000)
monkeypatch.setenv(launcher.PORT_RANGE_ENV, "40000-60000")
assert launcher.port_range() == (40000, 60000)
def test_port_range_is_unchecked_where_the_kernel_does_not_publish_one(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""No answer to compare against is not a reason to refuse a valid span."""
monkeypatch.setattr(
launcher, "EPHEMERAL_PORT_RANGE_FILE", str(tmp_path / "absent")
)
monkeypatch.setenv(launcher.PORT_RANGE_ENV, "40000-60000")
assert launcher.port_range() == (40000, 60000)
@pytest.mark.parametrize(
"value",
[
"40000", # not a range
"40000-", # missing end
"a-b", # not numbers
"40000-40050", # narrower than 100 ports
"60000-40000", # inverted (also narrower than 100)
"80-9000", # below 1024
"40000-70000", # above 65535
],
)
def test_port_range_refuses_a_range_upstream_would_reject(
monkeypatch: pytest.MonkeyPatch, value: str
) -> None:
# Upstream validates this too, but its ValidationError names
# docker.port_range_min — a TOML key nobody in this deployment ever wrote.
monkeypatch.setenv(launcher.PORT_RANGE_ENV, value)
with pytest.raises(launcher.SandboxServerConfigError, match=launcher.PORT_RANGE_ENV):
launcher.port_range()
def test_port_range_accepts_a_narrowed_firewall_span(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
_kernel_ephemeral_range(monkeypatch, tmp_path, 32768, 60999)
monkeypatch.setenv(launcher.PORT_RANGE_ENV, " 21000 - 21500 ")
assert launcher.port_range() == (21000, 21500)
def test_pids_limit_defaults_to_512() -> None:
# Deliberately below upstream's own 4096: a fork bomb inside one sandbox
# must not be able to exhaust the host's process table.
assert launcher.pids_limit() == 512
assert int(launcher.DEFAULT_PIDS_LIMIT) == 512
@pytest.mark.parametrize("value", ["0", "-1", "many"])
def test_pids_limit_refuses_a_non_positive_integer(
monkeypatch: pytest.MonkeyPatch, value: str
) -> None:
monkeypatch.setenv(launcher.PIDS_LIMIT_ENV, value)
with pytest.raises(launcher.SandboxServerConfigError, match=launcher.PIDS_LIMIT_ENV):
launcher.pids_limit()
def test_metadata_dir_derives_from_state_dir(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setattr(
"astrabox.config.settings.get_settings",
lambda: SimpleNamespace(resolved_state_dir=lambda: tmp_path / "state"),
)
assert launcher.metadata_dir() == tmp_path / "state" / launcher.DEFAULT_METADATA_DIRNAME
def test_metadata_dir_honours_its_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv(launcher.METADATA_DIR_ENV, str(tmp_path / "elsewhere"))
assert launcher.metadata_dir() == tmp_path / "elsewhere"
def test_publish_host_ip_accepts_an_explicit_address(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv(launcher.PUBLISH_HOST_IP_ENV, "172.17.0.1")
assert launcher.publish_host_ip() == "172.17.0.1"
def test_publish_host_ip_refuses_a_hostname(monkeypatch: pytest.MonkeyPatch) -> None:
# Docker's HostIp is an address, not a name, and this variable reaches the
# daemon verbatim.
monkeypatch.setenv(launcher.PUBLISH_HOST_IP_ENV, "host.docker.internal")
with pytest.raises(launcher.SandboxServerConfigError, match="must be an IP address"):
launcher.publish_host_ip()
def test_publish_host_ip_warns_when_told_to_publish_to_every_interface(
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
monkeypatch.setenv(launcher.PUBLISH_HOST_IP_ENV, "0.0.0.0")
with caplog.at_level(logging.WARNING, logger=launcher.__name__):
assert launcher.publish_host_ip() == "0.0.0.0"
assert "no authentication" in caplog.text
def test_publish_host_ip_defaults_to_loopback() -> None:
# A host-side lifecycle server can reach host-mapped ports on loopback. A
# containerized server sets the bridge gateway explicitly in Compose.
assert launcher.publish_host_ip() == "127.0.0.1"
assert launcher.DEFAULT_PUBLISH_HOST_IP == "127.0.0.1"
def test_publish_host_ip_needs_no_docker_daemon(monkeypatch: pytest.MonkeyPatch) -> None:
# Guards against a daemon dependency: publish_host_ip must never need the
# Docker daemon to resolve the sandbox network's gateway, because
# sandboxes are reached through the server proxy, never a host-mapped
# endpoint. Importing `docker` here must fail the test, not be tolerated.
monkeypatch.setitem(sys.modules, "docker", None)
assert launcher.publish_host_ip() == "127.0.0.1"
# ---------------------------------------------------------------------------
# the rendered document
# ---------------------------------------------------------------------------
@pytest.fixture
def document(monkeypatch: pytest.MonkeyPatch, state_dir: Path) -> dict[str, Any]:
return launcher.config_document()
def test_document_states_the_hardening_profile_explicitly(document: dict[str, Any]) -> None:
docker = document["docker"]
# Drop NET_RAW so a root process in a sandbox cannot open raw/packet
# sockets and craft traffic that bypasses the egress sidecar's output hook.
# Other capabilities upstream drops by default (SYS_PTRACE among them) are
# kept, because debugger and build workloads use them.
assert docker["drop_capabilities"] == ["NET_RAW"]
assert docker["drop_capabilities"] == list(launcher.SANDBOX_DROP_CAPABILITIES)
assert docker["no_new_privileges"] is True
assert docker["pids_limit"] == 512
assert docker["network_mode"] == "bridge"
assert (docker["port_range_min"], docker["port_range_max"]) == (20000, 32000)
# Left unset, so Docker's own default seccomp profile applies and no
# AppArmor profile is imposed.
assert "apparmor_profile" not in docker
assert "seccomp_profile" not in docker
def test_document_binds_loopback_and_sets_no_endpoint_host(
document: dict[str, Any],
) -> None:
assert document["server"]["host"] == "127.0.0.1"
assert document["server"]["port"] == int(launcher.DEFAULT_SERVER_PORT)
# `eip` must stay UNSET. Upstream uses it verbatim as the base of the
# server-proxy URL, while `docker.host_ip` is only the Docker host address.
# Upstream uses it verbatim as the base of the server-proxy URL it hands back
# (`f"{eip}/sandboxes/{id}/proxy/{port}"`), so an IP with no port there would
# point every sandbox's data plane, file panel and WebSocket at port 80 —
# created successfully, then silently unreachable.
assert "eip" not in document["server"]
assert document["docker"]["host_ip"] == "127.0.0.1"
def test_document_denies_every_host_bind_mount(document: dict[str, Any]) -> None:
# An empty allowlist REJECTS (upstream's shipped example config comments the
# opposite meaning for the same value); the open_sandbox backend never
# requests a host mount.
assert document["storage"]["allowed_host_paths"] == []
def test_document_keeps_upstreams_snapshot_store_out_of_home(
document: dict[str, Any], state_dir: Path
) -> None:
path = Path(document["store"]["path"])
assert path.name == launcher.SNAPSHOT_STORE_FILENAME
assert str(state_dir) in str(path)
def test_document_carries_no_resource_limits(document: dict[str, Any]) -> None:
"""Per-sandbox CPU/memory caps are a create-REQUEST field, not config.
There is nowhere in ``AppConfig`` to put them, so an env var here would be
inert; the gap is the provider's create call, not this document.
"""
flattened = json.dumps(document)
assert "resource" not in flattened
assert "mem_limit" not in flattened
def test_document_pins_the_docker_runtime_and_execd_image(document: dict[str, Any]) -> None:
assert document["runtime"] == {
"type": "docker",
"execd_image": launcher.DEFAULT_EXECD_IMAGE,
}
def test_execd_image_does_not_treat_an_agent_image_as_an_init_image(
monkeypatch: pytest.MonkeyPatch,
) -> None:
image = "registry.example/astrabox/agent:release-7"
monkeypatch.setenv("ASTRABOX_AGENT_IMAGE", image)
assert launcher.execd_image() == "opensandbox/execd:v1.1.0"
assert launcher.config_document()["runtime"]["execd_image"] == (
"opensandbox/execd:v1.1.0"
)
def test_explicit_execd_image_overrides_the_upstream_default(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("ASTRABOX_AGENT_IMAGE", "registry.example/agent:7")
monkeypatch.setenv(launcher.EXECD_IMAGE_ENV, "registry.example/execd:fixed")
assert launcher.execd_image() == "registry.example/execd:fixed"
def test_rendered_toml_round_trips_through_the_stdlib_parser(document: dict[str, Any]) -> None:
assert tomllib.loads(launcher.render_config_toml(document)) == document
def test_rendered_toml_escapes_a_string_that_would_break_the_file() -> None:
rendered = launcher.render_config_toml({"server": {"host": 'a"b\\c'}, "log": {"level": "INFO"}})
assert tomllib.loads(rendered)["server"]["host"] == 'a"b\\c'
def test_rendered_toml_refuses_a_value_it_cannot_render() -> None:
with pytest.raises(launcher.SandboxServerConfigError, match="cannot render"):
launcher.render_config_toml({"server": {"host": 1.5}})
def test_write_config_file_lands_beside_the_metadata_directory(
document: dict[str, Any], state_dir: Path
) -> None:
path = launcher.write_config_file(document)
assert path == launcher.metadata_dir().parent / launcher.CONFIG_FILENAME
assert tomllib.loads(path.read_text(encoding="utf-8")) == document
assert "DO NOT EDIT" in path.read_text(encoding="utf-8")
assert stat.S_IMODE(path.stat().st_mode) == 0o600
def test_write_config_file_fails_loud_on_an_unwritable_directory(
document: dict[str, Any], monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
blocker = tmp_path / "blocked"
blocker.write_text("not a directory", encoding="utf-8")
monkeypatch.setattr(launcher, "metadata_dir", lambda: blocker / "metadata")
with pytest.raises(launcher.SandboxServerConfigError, match=launcher.METADATA_DIR_ENV):
launcher.write_config_file(document)
# ---------------------------------------------------------------------------
# 2. the three compatibility hooks, against stubs carrying upstream's shape
# ---------------------------------------------------------------------------
def _networking_stub(*, sidecar_uses_normalizer: bool = True) -> ModuleType:
module = ModuleType("opensandbox_server.services.docker.networking")
module.normalize_port_bindings = lambda bindings: dict(bindings) # type: ignore[attr-defined]
module.EGRESS_SIDECAR_LABEL = "opensandbox.io/egress-sidecar-for" # type: ignore[attr-defined]
class DockerNetworkingMixin:
def _resolve_proxy_host(self) -> str:
return "127.0.0.1"
def _resolve_public_host(self) -> str:
return "127.0.0.1"
def _wait_for_egress_sidecar_ready(
self, sandbox_id: str, host_port: int, egress_token: str, timeout_seconds: float
) -> None:
return None
def _start_egress_sidecar(
self,
sandbox_id: str,
network_policy: Any,
egress_token: str,
host_execd_port: int,
host_http_port: int,
**_kwargs: Any,
) -> Any:
"""Upstream 0.2.3's order: image, policy, literal 0.0.0.0, host config."""
self._ensure_image_available(self.app_config.egress.image, None, sandbox_id) # type: ignore[attr-defined]
network_policy.model_dump(by_alias=True, exclude_none=True)
bindings = {
"44772": ("0.0.0.0", host_execd_port),
"8080": ("0.0.0.0", host_http_port),
}
if sidecar_uses_normalizer:
bindings = module.normalize_port_bindings(bindings) # type: ignore[attr-defined]
return self.docker_client.api.create_host_config( # type: ignore[attr-defined]
network_mode="bridge", port_bindings=bindings
)
module.DockerNetworkingMixin = DockerNetworkingMixin # type: ignore[attr-defined]
return module
def _recorded_sidecar_hosts(module: ModuleType) -> dict[str, str]:
bindings = launcher._record_egress_sidecar_port_bindings(
getattr(module, launcher.NETWORKING_MIXIN_ATTR)
)
return {port: str(binding[0]) for port, binding in bindings.items()}
def test_redirect_egress_sidecar_publish_host_narrows_execd_and_http() -> None:
module = _networking_stub()
assert _recorded_sidecar_hosts(module) == {"44772": "0.0.0.0", "8080": "0.0.0.0"}
launcher.redirect_egress_sidecar_publish_host(module, "172.17.0.1")
assert _recorded_sidecar_hosts(module) == {"44772": "172.17.0.1", "8080": "172.17.0.1"}
# A binding upstream already narrowed (the allocator's egress API port)
# passes through unchanged.
assert module.normalize_port_bindings({"18080": ("10.0.0.5", 3)}) == { # type: ignore[attr-defined]
"18080": ("10.0.0.5", 3)
}
def test_redirect_egress_sidecar_publish_host_fails_loud_when_the_sidecar_bypasses_it() -> None:
# The dangerous shape: the normalizer is still there and replaceable, but the
# sidecar stops passing its bindings through it, so every sandbox's execd
# would go back to 0.0.0.0 while the hook "succeeded".
module = _networking_stub(sidecar_uses_normalizer=False)
with pytest.raises(launcher.SandboxServerConfigError, match="ignored"):
launcher.redirect_egress_sidecar_publish_host(module, "172.17.0.1")
@pytest.mark.parametrize(
"attribute",
[
launcher.NETWORKING_MIXIN_ATTR,
launcher.SIDECAR_PORT_BINDINGS_ATTR,
launcher.START_EGRESS_SIDECAR_ATTR,
],
)
def test_redirect_egress_sidecar_publish_host_fails_loud_when_upstream_renames_it(
attribute: str,
) -> None:
module = _networking_stub()
if attribute == launcher.START_EGRESS_SIDECAR_ATTR:
delattr(module.DockerNetworkingMixin, attribute) # type: ignore[attr-defined]
else:
delattr(module, attribute)
with pytest.raises(launcher.SandboxServerConfigError, match=attribute):
launcher.redirect_egress_sidecar_publish_host(module, "172.17.0.1")
def test_redirect_egress_sidecar_publish_host_fails_loud_when_the_probe_breaks() -> None:
module = _networking_stub()
def _boom(*_args: Any, **_kwargs: Any) -> Any:
raise RuntimeError("upstream changed its signature")
module.DockerNetworkingMixin._start_egress_sidecar = _boom # type: ignore[attr-defined]
with pytest.raises(launcher.SandboxServerConfigError, match="failed before"):
launcher.redirect_egress_sidecar_publish_host(module, "172.17.0.1")
def test_redirect_proxy_host_uses_the_docker_host_address() -> None:
module = _networking_stub()
launcher.redirect_proxy_host(module, "172.17.0.1")
instance = object.__new__(module.DockerNetworkingMixin)
assert instance._resolve_proxy_host() == "172.17.0.1"
@pytest.mark.parametrize(
"attribute", [launcher.NETWORKING_MIXIN_ATTR, launcher.RESOLVE_PROXY_HOST_ATTR]
)
def test_redirect_proxy_host_fails_loud_when_upstream_renames_it(attribute: str) -> None:
module = _networking_stub()
if attribute == launcher.NETWORKING_MIXIN_ATTR:
delattr(module, attribute)
else:
delattr(module.DockerNetworkingMixin, attribute)
with pytest.raises(launcher.SandboxServerConfigError, match=attribute):
launcher.redirect_proxy_host(module, "172.17.0.1")
def test_redirect_public_endpoint_host_uses_the_published_port_address() -> None:
module = _networking_stub()
launcher.redirect_public_endpoint_host(module, "172.17.0.1")
instance = object.__new__(module.DockerNetworkingMixin)
assert instance._resolve_public_host() == "172.17.0.1"
@pytest.mark.parametrize(
"attribute", [launcher.NETWORKING_MIXIN_ATTR, launcher.RESOLVE_PUBLIC_HOST_ATTR]
)
def test_redirect_public_endpoint_host_fails_loud_when_upstream_renames_it(
attribute: str,
) -> None:
module = _networking_stub()
if attribute == launcher.NETWORKING_MIXIN_ATTR:
delattr(module, attribute)
else:
delattr(module.DockerNetworkingMixin, attribute)
with pytest.raises(launcher.SandboxServerConfigError, match=attribute):
launcher.redirect_public_endpoint_host(module, "172.17.0.1")
def _container_ops_stub() -> ModuleType:
module = ModuleType("opensandbox_server.services.docker.container_ops")
class DockerContainerOpsMixin:
def _create_and_start_container(self, *args: Any) -> Any:
return SimpleNamespace(id="sandbox-container")
module.DockerContainerOpsMixin = DockerContainerOpsMixin # type: ignore[attr-defined]
return module
def _constants_stub() -> ModuleType:
module = ModuleType("opensandbox_server.services.constants")
module.SANDBOX_EMBEDDING_PROXY_PORT_LABEL = "opensandbox.io/embedding-proxy-port" # type: ignore[attr-defined]
module.SANDBOX_HTTP_PORT_LABEL = "opensandbox.io/http-port" # type: ignore[attr-defined]
module.SandboxErrorCodes = SimpleNamespace( # type: ignore[attr-defined]
CONTAINER_START_FAILED="DOCKER::SANDBOX_START_FAILED",
INVALID_PARAMETER="SANDBOX::INVALID_PARAMETER",
)
return module
def _service_stub(networking: ModuleType, container_ops: ModuleType) -> ModuleType:
"""Upstream's shape: the allocator name the create path calls, and the class."""
module = ModuleType("opensandbox_server.services.docker.docker_service")
module.allocate_port_bindings = lambda ports, min_port=40000, max_port=60000: { # type: ignore[attr-defined]
port: ("0.0.0.0", min_port) for port in ports
}
class DockerSandboxService(
networking.DockerNetworkingMixin, # type: ignore[attr-defined,name-defined]
container_ops.DockerContainerOpsMixin, # type: ignore[attr-defined,name-defined]
):
def _expire_sandbox(self, sandbox_id: str) -> None:
return None
def _get_container_by_sandbox_id(self, sandbox_id: str) -> Any:
return None
def _remove_expiration_tracking(self, sandbox_id: str) -> None:
return None
def _cleanup_egress_sidecar(self, sandbox_id: str) -> None:
return None
module.DockerSandboxService = DockerSandboxService # type: ignore[attr-defined]
return module
def _volumes_stub() -> ModuleType:
module = ModuleType("opensandbox_server.services.docker.volumes")
class DockerVolumesMixin:
def _build_volume_binds(self, volumes: Any, pvc_inspect_cache: Any = None) -> list[str]:
return []
module.DockerVolumesMixin = DockerVolumesMixin # type: ignore[attr-defined]
return module
def _port_assignment_stubs() -> tuple[ModuleType, ModuleType, ModuleType, ModuleType]:
networking = _networking_stub()
container_ops = _container_ops_stub()
return _service_stub(networking, container_ops), networking, container_ops, _constants_stub()
class _Published:
"""A started container as docker-py describes it after ``reload()``."""
def __init__(self, ports: dict[str, Any]) -> None:
self.attrs = {"NetworkSettings": {"Ports": ports}}
def reload(self) -> None:
return None
def test_published_host_ports_reads_one_port_per_container_port() -> None:
# Docker lists an IPv4 and an IPv6 binding of the same host port separately.
container = _Published(
{
"44772/tcp": [
{"HostIp": "0.0.0.0", "HostPort": "20011"},
{"HostIp": "::", "HostPort": "20011"},
],
"8080/tcp": [{"HostIp": "172.17.0.1", "HostPort": "20012"}],
}
)
assert launcher._published_host_ports(container, ("44772", "8080")) == {
"44772": 20011,
"8080": 20012,
}
@pytest.mark.parametrize(
"bindings",
[
None,
[],
[{"HostIp": "172.17.0.1", "HostPort": "20000-32000"}],
[
{"HostIp": "172.17.0.1", "HostPort": "20011"},
{"HostIp": "172.17.0.1", "HostPort": "20012"},
],
],
)
def test_published_host_ports_refuses_a_port_docker_did_not_publish_once(
bindings: Any,
) -> None:
# A range, a missing port or two different ports cannot become the one
# number a sandbox's endpoint is resolved from.
container = _Published({"44772/tcp": bindings})
with pytest.raises(RuntimeError, match="44772/tcp"):
launcher._published_host_ports(container, ("44772",))
@pytest.mark.parametrize(
("module_index", "attribute"),
[
(0, launcher.SERVICE_ALLOCATE_PORTS_ATTR),
(0, launcher.SERVICE_CLS_ATTR),
(1, launcher.NETWORKING_MIXIN_ATTR),
(2, launcher.CONTAINER_OPS_MIXIN_ATTR),
(3, launcher.EXECD_PORT_LABEL_ATTR),
(3, launcher.HTTP_PORT_LABEL_ATTR),
(3, launcher.ERROR_CODES_ATTR),
],
)
def test_let_docker_assign_published_ports_fails_loud_when_upstream_renames_it(
module_index: int, attribute: str
) -> None:
modules = _port_assignment_stubs()
delattr(modules[module_index], attribute)
with pytest.raises(launcher.SandboxServerConfigError, match=attribute):
launcher.let_docker_assign_published_ports(*modules, "172.17.0.1", ports=(20000, 32000))
@pytest.mark.parametrize(
"attribute",
[
launcher.START_EGRESS_SIDECAR_ATTR,
launcher.WAIT_FOR_SIDECAR_ATTR,
launcher.CREATE_AND_START_ATTR,
],
)
def test_let_docker_assign_published_ports_fails_loud_when_a_method_is_gone(
attribute: str,
) -> None:
service, networking, container_ops, constants = _port_assignment_stubs()
owner = (
container_ops.DockerContainerOpsMixin # type: ignore[attr-defined]
if attribute == launcher.CREATE_AND_START_ATTR
else networking.DockerNetworkingMixin # type: ignore[attr-defined]
)
delattr(owner, attribute)
with pytest.raises(launcher.SandboxServerConfigError, match=attribute):
launcher.let_docker_assign_published_ports(
service, networking, container_ops, constants, "172.17.0.1", ports=(20000, 32000)
)
def test_let_docker_assign_published_ports_fails_loud_when_the_service_overrides_it() -> None:
# A method the service class defines itself hides the mixin's wrapper, so
# upstream would keep choosing ports while the hook reported success.
service, networking, container_ops, constants = _port_assignment_stubs()
def _own_start(self: Any, **_kwargs: Any) -> Any:
return None
setattr(service.DockerSandboxService, launcher.START_EGRESS_SIDECAR_ATTR, _own_start) # type: ignore[attr-defined]
with pytest.raises(launcher.SandboxServerConfigError, match="overrides"):
launcher.let_docker_assign_published_ports(
service, networking, container_ops, constants, "172.17.0.1", ports=(20000, 32000)
)
def _metadata_stub(
*,
honours_constant: bool = True,
writable: bool = True,
corrupt_read: bool = False,
home: Path | None = None,
) -> ModuleType:
"""A stub with upstream's shape: a store defaulting to a module constant.
``home`` stands in for upstream's ``$HOME``-rooted default. It is a real,
WRITABLE directory on purpose: the failure being nailed is a store that
happily writes somewhere else, which is invisible unless the probe checks
where the bytes landed rather than whether the write raised.
"""
module = ModuleType("opensandbox_server.services.docker.metadata")
upstream_home = home or Path("/nonexistent/home/.opensandbox/metadata")
module.DEFAULT_STORE_DIR = upstream_home # type: ignore[attr-defined]
class DockerMetadataStore:
def __init__(self, root: Path | None = None) -> None:
fallback = module.DEFAULT_STORE_DIR if honours_constant else upstream_home
self._root = root or fallback
def _path(self, sandbox_id: str) -> Path:
return Path(self._root) / f"{sandbox_id}.expiration.json"
def set_expiration(self, sandbox_id: str, when: Any) -> None:
if not writable:
raise OSError(13, "Permission denied")
path = self._path(sandbox_id)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps({"expires_at": when.isoformat()}), encoding="utf-8")
def get_expiration(self, sandbox_id: str) -> str | None:
if corrupt_read:
return "1999-01-01T00:00:00+00:00"
path = self._path(sandbox_id)
if not path.exists():
return None
value = json.loads(path.read_text(encoding="utf-8"))["expires_at"]
return str(value)
def delete(self, sandbox_id: str) -> None:
self._path(sandbox_id).unlink(missing_ok=True)
module.DockerMetadataStore = DockerMetadataStore # type: ignore[attr-defined]
return module
def test_redirect_metadata_store_root_rebinds_and_proves_a_round_trip(tmp_path: Path) -> None:
module = _metadata_stub()
root = tmp_path / "metadata"
root.mkdir()
launcher.redirect_metadata_store_root(module, root)
assert module.DEFAULT_STORE_DIR == root
# The probe cleans up after itself: a lifecycle server that later lists the
# directory must not see AstraBox's startup check.
assert list(root.iterdir()) == []
@pytest.mark.parametrize(
"attribute", [launcher.METADATA_DEFAULT_ROOT_ATTR, launcher.METADATA_STORE_CLS_ATTR]
)
def test_redirect_metadata_store_root_fails_loud_when_upstream_renames_it(
attribute: str, tmp_path: Path
) -> None:
module = _metadata_stub()
delattr(module, attribute)
with pytest.raises(launcher.SandboxServerConfigError, match=attribute):
launcher.redirect_metadata_store_root(module, tmp_path)
def test_redirect_metadata_store_root_fails_loud_when_the_store_ignores_the_constant(
tmp_path: Path,
) -> None:
# Lease renewals would keep going to $HOME, and a restart would kill live
# sandboxes at the lease they were born with.
home = tmp_path / "home" / ".opensandbox" / "metadata"
target = tmp_path / "metadata"
target.mkdir()
module = _metadata_stub(honours_constant=False, home=home)
with pytest.raises(launcher.SandboxServerConfigError, match="left nothing under"):
launcher.redirect_metadata_store_root(module, target)
# The write SUCCEEDED — it just went to upstream's $HOME default, which is
# exactly the failure a "did the write raise?" check cannot see.
assert home.exists()
assert list(target.iterdir()) == []
def test_redirect_metadata_store_root_refuses_an_unwritable_directory(tmp_path: Path) -> None:
module = _metadata_stub(writable=False)
with pytest.raises(launcher.SandboxServerConfigError, match="not writable"):
launcher.redirect_metadata_store_root(module, tmp_path)
def test_redirect_metadata_store_root_refuses_a_store_that_reads_back_wrong(
tmp_path: Path,
) -> None:
module = _metadata_stub(corrupt_read=True)
with pytest.raises(launcher.SandboxServerConfigError, match="did not round-trip"):
launcher.redirect_metadata_store_root(module, tmp_path)
# ---------------------------------------------------------------------------
# the ephemeral-store warning
# ---------------------------------------------------------------------------
def test_ephemeral_metadata_dir_warns_with_the_consequence(
caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
# The single most likely way to hit this: `docker run` with no volume, so the
# metadata lands on the container's own overlay root.
monkeypatch.setattr(launcher, "_filesystem_type", lambda _path: "overlay")
with caplog.at_level(logging.WARNING, logger=launcher.__name__):
launcher.warn_if_metadata_dir_is_ephemeral(tmp_path)
# Not "metadata may be lost": the consequence is that a restart kills live,
# repeatedly-renewed sandboxes at their birth lease.
assert "birth" in caplog.text
assert "RENEWAL" in caplog.text
def test_boot_cleared_metadata_dir_warns_even_on_a_persistent_filesystem(
caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(launcher, "_filesystem_type", lambda _path: "ext4")
with caplog.at_level(logging.WARNING, logger=launcher.__name__):
launcher.warn_if_metadata_dir_is_ephemeral(Path("/tmp/astrabox/opensandbox/metadata"))
assert "cleared on boot" in caplog.text
def test_persistent_metadata_dir_is_quiet(
caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(launcher, "_filesystem_type", lambda _path: "ext4")
with caplog.at_level(logging.WARNING, logger=launcher.__name__):
launcher.warn_if_metadata_dir_is_ephemeral(Path("/data/opensandbox/metadata"))
assert caplog.text == ""
def test_filesystem_type_reads_the_longest_matching_mount() -> None:
"""The real /proc/mounts reader, on a path that certainly has a mount."""
assert launcher._filesystem_type(Path("/")) is not None
# ---------------------------------------------------------------------------
# the optional extra
# ---------------------------------------------------------------------------
class _BlockUpstream:
"""A meta-path finder that makes ``opensandbox_server`` un-importable.
Lets the missing-extra behaviour be asserted in an environment where the
extra IS installed, so the test means the same thing in both lanes.
"""
def find_module(self, name: str, path: Any = None) -> Any: # pragma: no cover - legacy hook
return None
def find_spec(self, name: str, path: Any = None, target: Any = None) -> Any:
if name == "opensandbox_server" or name.startswith("opensandbox_server."):
raise ImportError(f"blocked for the test: {name}")
return None
@pytest.fixture
def without_extra(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
blocker = _BlockUpstream()
monkeypatch.setattr(sys, "meta_path", [blocker, *sys.meta_path])
for name in [n for n in sys.modules if n.startswith("opensandbox_server")]:
monkeypatch.delitem(sys.modules, name, raising=False)
yield
def test_importing_the_launcher_never_needs_the_extra(without_extra: None) -> None:
"""The clean-boot invariant: ``import astrabox`` must not need an extra."""
module = importlib.reload(sys.modules["astrabox.deploy.sandbox_server"])
assert module.EXTRA_NAME == "sandbox-server"
def test_prepare_reports_the_missing_extra_actionably(
without_extra: None,
document: dict[str, Any],
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(launcher, "config_document", lambda: document)
with pytest.raises(launcher.SandboxServerConfigError, match=r"astrabox\[sandbox-server\]"):
launcher.prepare()
# ---------------------------------------------------------------------------
# 3. the REAL upstream modules (skipped when the extra is absent)
# ---------------------------------------------------------------------------
def _upstream(name: str) -> Any:
return pytest.importorskip(
name,
reason="needs the sandbox-server extra (pip install -e '.[sandbox-server]')",
)
def test_upstream_still_chooses_host_ports_before_docker_does() -> None:
"""The reason :func:`let_docker_assign_published_ports` exists, upstream.
The allocator the create path calls hands Docker a fixed number it chose by
binding a socket in its own network namespace. If this fails because
upstream now lets Docker choose, delete the hook instead of updating this
test.
"""
service = _upstream("opensandbox_server.services.docker.docker_service")
allocator = _upstream("opensandbox_server.services.docker.port_allocator")
allocate = getattr(service, launcher.SERVICE_ALLOCATE_PORTS_ATTR)
assert allocate is allocator.allocate_port_bindings
_host, port = allocate(["8080"], min_port=40000, max_port=60000)["8080"]
assert isinstance(port, int)
class _FakeDockerDaemon:
"""Docker's side of a create, for upstream's real provisioning code.
``held`` are host ports something outside the lifecycle server's network
namespace holds: invisible to a probe the server runs, visible to Docker,
which binds in the host namespace. A range binding gets the lowest port of
its range that is neither held nor already published, as the daemon's
allocator does; a fixed binding on a held port fails the way Docker does.
"""
def __init__(self, held: Iterable[int]) -> None:
self.held = set(held)
self.created: dict[str, dict[str, Any]] = {}
self.published: dict[str, dict[str, list[dict[str, str]]]] = {}
self.removed: list[str] = []
self.api = SimpleNamespace(
create_host_config=lambda **kwargs: dict(kwargs),
create_container=self._create_container,
remove_container=lambda container_id, force=False: self.removed.append(container_id),
inspect_volume=lambda name: {"Labels": {}},
remove_volume=lambda name: None,
)
self.volumes = SimpleNamespace(create=lambda **kwargs: SimpleNamespace(**kwargs))
self.containers = SimpleNamespace(get=self._container, list=self._list)
def _create_container(self, **kwargs: Any) -> dict[str, str]:
container_id = f"container-{len(self.created)}"
self.created[container_id] = kwargs
return {"Id": container_id}
def add(self, name: str, labels: dict[str, str]) -> None:
"""A container that already exists when the lifecycle server starts."""
self.created[name] = {"name": name, "labels": labels, "host_config": {}}
def _list(self, all: bool = False, filters: dict[str, Any] | None = None) -> list[Any]:
selector = str((filters or {}).get("label") or "")
key, _, value = selector.partition("=")
return [
self._container(container_id)
for container_id, record in self.created.items()
if container_id not in self.removed
and (
not key
or (
key in (record.get("labels") or {})
and (not value or record["labels"][key] == value)
)
)
]
def _container(self, reference: str) -> Any:
from docker.errors import NotFound
daemon = self
container_id = next(
(
candidate
for candidate, record in self.created.items()
if reference in (candidate, record.get("name"))
),
None,
)
if container_id is None or container_id in self.removed:
raise NotFound(f"No such container: {reference}")
record = self.created[container_id]
class _Container:
id = container_id
@property
def labels(self) -> dict[str, str]:
return dict(record.get("labels") or {})
@property
def attrs(self) -> dict[str, Any]:
return {
"Config": {"Labels": record.get("labels") or {}},
"NetworkSettings": {"Ports": daemon.published.get(container_id, {})},
"State": {"Running": True},
}
def reload(self) -> None:
return None
def kill(self) -> None:
return None
def start(self) -> None:
daemon._publish(container_id, record["host_config"].get("port_bindings") or {})
def remove(self, force: bool = False) -> None:
daemon.removed.append(container_id)
return _Container()
def _publish(self, container_id: str, bindings: dict[str, Any]) -> None:
from docker.errors import APIError
in_use = self.held | {
int(binding["HostPort"])
for ports in self.published.values()
for published in ports.values()
for binding in published
}
ports: dict[str, list[dict[str, str]]] = {}
for container_port, (host_ip, host_port) in bindings.items():
low, _, high = str(host_port).partition("-")
candidates = range(int(low), int(high or low) + 1)
free = [port for port in candidates if port not in in_use]
if not free:
raise APIError(
"500 Server Error: failed to set up container networking: "
f"failed to bind host port {host_ip}:{candidates[-1]}/tcp: "
"address already in use"
)
in_use.add(free[0])
key = container_port if "/" in container_port else f"{container_port}/tcp"
ports[key] = [{"HostIp": host_ip, "HostPort": str(free[0])}]
self.published[container_id] = ports
@pytest.fixture
def upstream_docker_service(
document: dict[str, Any], monkeypatch: pytest.MonkeyPatch
) -> Iterator[Any]:
"""Upstream's real Docker service class with every hook this deployment applies.
Each patched upstream attribute is registered with ``monkeypatch`` first, so
the real classes are restored after the test.
"""
service = _upstream("opensandbox_server.services.docker.docker_service")
networking = _upstream("opensandbox_server.services.docker.networking")
container_ops = _upstream("opensandbox_server.services.docker.container_ops")
volumes = _upstream("opensandbox_server.services.docker.volumes")
constants = _upstream("opensandbox_server.services.constants")
config_module = _upstream("opensandbox_server.config")
for owner, attribute in (
(service, launcher.SERVICE_ALLOCATE_PORTS_ATTR),
(networking, launcher.SIDECAR_PORT_BINDINGS_ATTR),
(networking.DockerNetworkingMixin, launcher.START_EGRESS_SIDECAR_ATTR),
(container_ops.DockerContainerOpsMixin, launcher.CREATE_AND_START_ATTR),
(volumes.DockerVolumesMixin, launcher.BUILD_VOLUME_BINDS_ATTR),
):
monkeypatch.setattr(owner, attribute, getattr(owner, attribute))
document["docker"]["host_ip"] = "172.17.0.1"
document["docker"]["port_range_min"] = 20000
document["docker"]["port_range_max"] = 20100
launcher.redirect_egress_sidecar_publish_host(networking, "172.17.0.1")
launcher.let_docker_assign_published_ports(
service, networking, container_ops, constants, "172.17.0.1", ports=(20000, 20100)
)
launcher.mount_volume_subpaths_through_docker(volumes, container_ops)
instance = object.__new__(service.DockerSandboxService)
instance.app_config = config_module.AppConfig(**document)
instance.network_mode = "bridge"
instance.docker_runtime = None
instance.readiness_ports = []
instance._ensure_image_available = lambda *_args, **_kwargs: None
instance._prepare_sandbox_runtime = lambda *_args, **_kwargs: None
instance._resolve_platform_for_container = lambda *_args, **_kwargs: None
def _record_readiness_port(
sandbox_id: str, host_port: int, token: str, timeout_seconds: float
) -> None:
instance.readiness_ports.append(host_port)
instance._wait_for_egress_sidecar_ready = _record_readiness_port
yield instance
def _provision(
instance: Any,
daemon: _FakeDockerDaemon,
*,
network_policy: bool = True,
volumes: list[dict[str, Any]] | None = None,
pvc_inspect_cache: dict[str, dict[str, Any]] | None = None,
) -> Any:
schema = _upstream("opensandbox_server.api.schema")
instance.docker_client = daemon
request = schema.CreateSandboxRequest(
image={"uri": "astrabox/sandbox:test"},
entrypoint=["/bin/sleep", "infinity"],
resourceLimits={"cpu": "1", "memory": "1Gi"},
**({"networkPolicy": {"defaultAction": "allow"}} if network_policy else {}),
**({"volumes": volumes} if volumes else {}),
)
return instance._provision_sandbox(
"sbx", request, datetime.now(timezone.utc), None, pvc_inspect_cache
)
def test_a_sandbox_records_the_ports_docker_published_while_the_host_holds_the_rest(
upstream_docker_service: Any,
) -> None:
"""Every port but three is held outside the server's namespace.
A probe in the server's own namespace sees all of them free, so a number it
chose would almost surely be one Docker cannot publish. Docker's choice is
the only one that lands on the three free ports, and the sandbox's endpoint
labels must name exactly what Docker published.
"""
free = {20031, 20064, 20097}
daemon = _FakeDockerDaemon(held=set(range(20000, 20101)) - free)
_provision(upstream_docker_service, daemon)
sidecar_id, sandbox_id = list(daemon.created)
sidecar = daemon.published[sidecar_id]
assert daemon.created[sidecar_id]["host_config"]["port_bindings"] == {
"44772": ("172.17.0.1", "20000-20100"),
"8080": ("172.17.0.1", "20000-20100"),
"18080": ("172.17.0.1", "20000-20100"),
}
execd, http, egress_api = (
int(sidecar[f"{port}/tcp"][0]["HostPort"]) for port in ("44772", "8080", "18080")
)
assert {execd, http, egress_api} == free
labels = daemon.created[sandbox_id]["labels"]
assert labels["opensandbox.io/embedding-proxy-port"] == str(execd)
assert labels["opensandbox.io/http-port"] == str(http)
assert upstream_docker_service.readiness_ports == [egress_api]
assert daemon.removed == []
def test_a_sidecar_docker_cannot_publish_reports_dockers_reason(
upstream_docker_service: Any,
) -> None:
# Every port is held: no mechanism can publish, and the caller must learn
# why from the create response rather than from the server log.
from fastapi import HTTPException
daemon = _FakeDockerDaemon(held=range(20000, 20101))
with pytest.raises(HTTPException) as raised:
_provision(upstream_docker_service, daemon)
message = raised.value.detail["message"]
assert message.startswith("Egress sidecar container failed to start")
assert "address already in use" in message
assert list(daemon.created) == ["container-0"]
assert daemon.removed == ["container-0"]
def test_a_sandbox_without_a_network_policy_is_refused_before_any_container(
upstream_docker_service: Any,
) -> None:
# Such a sandbox would publish its own ports, and its endpoint labels are
# written before Docker chooses them.
from fastapi import HTTPException
daemon = _FakeDockerDaemon(held=())
with pytest.raises(HTTPException) as raised:
_provision(upstream_docker_service, daemon, network_policy=False)
assert raised.value.status_code == 400
assert "networkPolicy" in raised.value.detail["message"]
assert daemon.created == {}
def test_a_restarted_server_leaves_another_installations_sandbox_running(
document: dict[str, Any], monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Both sandboxes are past their create-time lease; only one is this server's.
The other installation renews its sandbox through its own lifecycle
server, so this server's restart must not expire it from the stale label.
"""
lapsed = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
daemon = _FakeDockerDaemon(held=())
for sandbox_id, installation in (("ours", "this-installation"), ("theirs", "other")):
daemon.add(
f"sandbox-{sandbox_id}",
{
"opensandbox.io/id": sandbox_id,
"opensandbox.io/expires-at": lapsed,
"astrabox.installation": installation,
},
)
_started_service(document, monkeypatch, tmp_path, daemon)
assert daemon.removed == ["sandbox-ours"]
_WORKSPACE_VIEW = [
{
"name": "astrabox-workspace-0",
"pvc": {"claimName": "astrabox-view-0123"},
"mountPath": "/workspace",
"subPath": "0/workspace",
}
]
def test_a_volume_subpath_reaches_docker_as_a_volume_mount(
upstream_docker_service: Any,
) -> None:
"""A workspace view's data exists only while Docker has the volume mounted.
Its Mountpoint is an empty directory otherwise, so a bind of
Mountpoint/subPath gives the sandbox that empty directory. Docker must be
asked for the volume with the sub-path.
"""
daemon = _FakeDockerDaemon(held=())
_provision(
upstream_docker_service,
daemon,
volumes=_WORKSPACE_VIEW,
pvc_inspect_cache={
"astrabox-view-0123": {
"Driver": "local",
"Mountpoint": "/var/lib/docker/volumes/astrabox-view-0123/_data",
}
},
)
_sidecar, sandbox = list(daemon.created)
host_config = daemon.created[sandbox]["host_config"]
assert not any("astrabox-view-0123" in bind for bind in host_config.get("binds") or [])
assert [dict(mount) for mount in host_config["mounts"]] == [
{
"Target": "/workspace",
"Source": "astrabox-view-0123",
"Type": "volume",
"ReadOnly": False,
"VolumeOptions": {"Subpath": "0/workspace"},
}
]
def _started_service(
document: dict[str, Any],
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
daemon: _FakeDockerDaemon,
) -> Any:
"""Upstream's real service, constructed (and so restored) against ``daemon``."""
service = _upstream("opensandbox_server.services.docker.docker_service")
networking = _upstream("opensandbox_server.services.docker.networking")
metadata = _upstream("opensandbox_server.services.docker.metadata")
config_module = _upstream("opensandbox_server.config")
monkeypatch.setattr(metadata, launcher.METADATA_DEFAULT_ROOT_ATTR, tmp_path / "metadata")
for attribute in (
"__init__",
launcher.EXPIRE_SANDBOX_ATTR,
launcher.CLEANUP_SIDECAR_ATTR,
):
monkeypatch.setattr(
service.DockerSandboxService,
attribute,
getattr(service.DockerSandboxService, attribute),
)
launcher.leave_other_installations_sandboxes_running(service, networking, "this-installation")
def _from_env(timeout: int | None = None) -> _FakeDockerDaemon:
return daemon
monkeypatch.setattr(service.docker, "from_env", _from_env)
return service.DockerSandboxService(config=config_module.AppConfig(**document))
def test_a_restarted_server_leaves_another_installations_sidecar(
document: dict[str, Any], monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Sidecars with no sandbox container: this installation's leftover, and
another installation's create in progress (its sidecar starts first)."""
daemon = _FakeDockerDaemon(held=())
for sandbox_id, installation in (("ours", "this-installation"), ("theirs", "other")):
daemon.add(
f"sandbox-egress-{sandbox_id}",
{
"opensandbox.io/egress-sidecar-for": sandbox_id,
"astrabox.installation": installation,
},
)
_started_service(document, monkeypatch, tmp_path, daemon)
assert daemon.removed == ["sandbox-egress-ours"]
def test_the_server_labels_every_sidecar_it_creates_with_its_installation(
document: dict[str, Any], monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
daemon = _FakeDockerDaemon(held=())
instance = _started_service(document, monkeypatch, tmp_path, daemon)
instance._ensure_image_available = lambda *_args, **_kwargs: None
instance._prepare_sandbox_runtime = lambda *_args, **_kwargs: None
instance._resolve_platform_for_container = lambda *_args, **_kwargs: None
instance._wait_for_egress_sidecar_ready = lambda *_args, **_kwargs: None
_provision(instance, daemon)
sidecar = next(
record
for record in daemon.created.values()
if "opensandbox.io/egress-sidecar-for" in (record.get("labels") or {})
)
assert sidecar["labels"]["astrabox.installation"] == "this-installation"
def test_upstream_proxy_host_ignores_docker_host_ip_when_server_binds_loopback() -> None:
module = _upstream("opensandbox_server.services.docker.networking")
mixin = getattr(module, launcher.NETWORKING_MIXIN_ATTR)
instance = object.__new__(mixin)
instance.app_config = SimpleNamespace(
server=SimpleNamespace(host="127.0.0.1"),
docker=SimpleNamespace(host_ip="172.17.0.1"),
)
assert instance._resolve_proxy_host() == "127.0.0.1"
def test_redirect_proxy_host_works_against_the_real_upstream_mixin() -> None:
module = _upstream("opensandbox_server.services.docker.networking")
mixin = getattr(module, launcher.NETWORKING_MIXIN_ATTR)
original = getattr(mixin, launcher.RESOLVE_PROXY_HOST_ATTR)
try:
launcher.redirect_proxy_host(module, "172.17.0.1")
instance = object.__new__(mixin)
assert instance._resolve_proxy_host() == "172.17.0.1"
finally:
setattr(mixin, launcher.RESOLVE_PROXY_HOST_ATTR, original)
def test_redirect_public_endpoint_host_works_against_the_real_upstream_mixin() -> None:
module = _upstream("opensandbox_server.services.docker.networking")
mixin = getattr(module, launcher.NETWORKING_MIXIN_ATTR)
original = getattr(mixin, launcher.RESOLVE_PUBLIC_HOST_ATTR)
try:
launcher.redirect_public_endpoint_host(module, "172.17.0.1")
instance = object.__new__(mixin)
assert instance._resolve_public_host() == "172.17.0.1"
finally:
setattr(mixin, launcher.RESOLVE_PUBLIC_HOST_ATTR, original)
def test_upstream_egress_sidecar_still_publishes_execd_on_every_interface() -> None:
"""The reason :func:`redirect_egress_sidecar_publish_host` exists, upstream.
If this fails because upstream now honours a publish address (its
``[docker] publish_host``), render that field and delete the hook instead of
updating this test.
"""
module = _upstream("opensandbox_server.services.docker.networking")
assert _recorded_sidecar_hosts(module) == {"44772": "0.0.0.0", "8080": "0.0.0.0"}
def test_redirect_egress_sidecar_publish_host_works_against_the_real_upstream_sidecar() -> None:
module = _upstream("opensandbox_server.services.docker.networking")
original = getattr(module, launcher.SIDECAR_PORT_BINDINGS_ATTR)
try:
launcher.redirect_egress_sidecar_publish_host(module, "172.17.0.1")
hosts = _recorded_sidecar_hosts(module)
finally:
setattr(module, launcher.SIDECAR_PORT_BINDINGS_ATTR, original)
assert hosts["44772"] == "172.17.0.1"
assert hosts["8080"] == "172.17.0.1"
def test_upstream_metadata_store_still_defaults_under_home() -> None:
module = _upstream("opensandbox_server.services.docker.metadata")
default = getattr(module, launcher.METADATA_DEFAULT_ROOT_ATTR)
assert Path.home() in Path(default).parents
assert hasattr(module, launcher.METADATA_STORE_CLS_ATTR)
def test_upstream_service_still_builds_its_store_with_no_argument() -> None:
"""The rebind's premise: there is no injection point for the store root.
``DockerMetadataStore.__init__`` accepts a root but the service passes none,
and the service's own constructor already reads that store, so a
post-construction swap would read the wrong directory on exactly the restart
path the redirection exists to fix.
"""
import inspect
service = _upstream("opensandbox_server.services.docker.docker_service")
source = inspect.getsource(service.DockerSandboxService.__init__)
assert f"{launcher.METADATA_STORE_CLS_ATTR}()" in source
def test_upstream_config_accepts_the_document_astrabox_renders(
document: dict[str, Any],
) -> None:
config_module = _upstream("opensandbox_server.config")
config = config_module.AppConfig(**document)
assert config.server.host == "127.0.0.1"
assert config.docker.drop_capabilities == ["NET_RAW"]
assert config.docker.pids_limit == 512
# `eip` remains unset. `docker.host_ip` gives a containerized lifecycle
# server a routable address for Docker host-port mappings.
assert config.server.eip is None
assert config.docker.host_ip == "127.0.0.1"
def test_upstream_server_proxy_url_would_swallow_an_eip_without_a_port() -> None:
"""Why ``server.eip`` must stay unset, asserted against upstream's source.
The endpoint route builds the server-proxy URL by pasting ``server.eip``
straight in front of the path, with no port of its own. Since AstraBox asks
for server-proxied endpoints, setting ``eip`` to a bare host address (the
natural reading of "bound public IP") would hand back
``<ip>/sandboxes/<id>/proxy/<port>`` — scheme-default port 80, nothing
listening, every sandbox created fine and then silently unreachable.
If this stops matching because upstream started composing a port, revisit
:func:`astrabox.deploy.sandbox_server.config_document`; until then the field
stays out of the document.
"""
import ast
import importlib.util
# Read, not import: the lifecycle module loads the server's config file at
# import time, and this assertion is about its source, not a running server.
_upstream("opensandbox_server")
spec = importlib.util.find_spec("opensandbox_server.api.lifecycle")
assert spec is not None and spec.origin is not None
module_source = Path(spec.origin).read_text(encoding="utf-8")
source = next(
ast.get_source_segment(module_source, node) or ""
for node in ast.walk(ast.parse(module_source))
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and node.name == "get_sandbox_endpoint"
)
assert "server.eip" in source
assert 'f"{base_url}/sandboxes/{sandbox_id}/proxy/{port}"' in source
def test_upstream_has_no_server_side_resource_defaults() -> None:
"""Why there is no CPU/memory env var: there is nowhere to put one."""
config_module = _upstream("opensandbox_server.config")
fields = set(config_module.DockerConfig.model_fields)
assert not {name for name in fields if "mem" in name or "cpu" in name}
# ---------------------------------------------------------------------------
# 4. the Kubernetes runtime
#
# Same three layers as above, one runtime over: what the document must say, what
# must be REFUSED, and the startup proof — the last one against stubs carrying
# the Kubernetes client's shape, so it fires in the unit lane with neither the
# optional extra nor a cluster.
# ---------------------------------------------------------------------------
@pytest.fixture
def kubernetes(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv(launcher.RUNTIME_ENV, launcher.RUNTIME_KUBERNETES)
def test_runtime_defaults_to_docker_and_accepts_kubernetes(
monkeypatch: pytest.MonkeyPatch,
) -> None:
assert launcher.sandbox_runtime() == launcher.RUNTIME_DOCKER
monkeypatch.setenv(launcher.RUNTIME_ENV, "KUBERNETES")
assert launcher.sandbox_runtime() == launcher.RUNTIME_KUBERNETES
def test_runtime_refuses_anything_upstream_does_not_implement(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Upstream's own RuntimeConfig.type is a two-value Literal; a typo must name
# the AstraBox variable, not arrive as a pydantic error about runtime.type.
monkeypatch.setenv(launcher.RUNTIME_ENV, "k8s")
with pytest.raises(launcher.SandboxServerConfigError, match=launcher.RUNTIME_ENV):
launcher.sandbox_runtime()
def test_docker_document_includes_the_default_credential_sidecar(
state_dir: Path,
) -> None:
"""Pin the rendered file, including protected delivery's default sidecar."""
expected = f"""\
# Generated by astrabox.deploy.sandbox_server — DO NOT EDIT.
# Rewritten from the ASTRABOX_* environment on every start; edits are lost.
[server]
host = "127.0.0.1"
port = 8990
[log]
level = "INFO"
[runtime]
type = "docker"
execd_image = "opensandbox/execd:v1.1.0"
[docker]
network_mode = "bridge"
drop_capabilities = ["NET_RAW"]
no_new_privileges = true
pids_limit = 512
port_range_min = 20000
port_range_max = 32000
host_ip = "127.0.0.1"
[storage]
allowed_host_paths = []
[store]
type = "sqlite"
path = "{state_dir / "opensandbox" / "opensandbox.db"}"
[egress]
image = "opensandbox/egress:v1.1.7"
mode = "dns+nft"
disable_ipv6 = true
"""
assert launcher.render_config_toml(launcher.config_document()) == expected
def test_kubernetes_document_carries_the_kubernetes_block_and_no_docker_one(
kubernetes: None, state_dir: Path
) -> None:
document = launcher.config_document()
assert document["runtime"] == {
"type": "kubernetes",
"execd_image": launcher.DEFAULT_EXECD_IMAGE,
}
# Upstream's AppConfig validator makes the two blocks exclusive, and every
# docker field describes something Kubernetes does not have.
assert "docker" not in document
template_path = (
state_dir / "opensandbox" / launcher.BATCHSANDBOX_TEMPLATE_FILENAME
)
assert document["kubernetes"] == {
"batchsandbox_template_file": str(template_path),
"namespace": launcher.DEFAULT_KUBE_NAMESPACE,
"workload_provider": launcher.DEFAULT_KUBE_WORKLOAD_PROVIDER,
"image_pull_policy": launcher.DEFAULT_KUBE_IMAGE_PULL_POLICY,
"informer_enabled": True,
"sandbox_create_timeout_seconds": 60,
}
# `direct` decides the SHAPE of a resolved endpoint (<pod IP>:<port>), so it
# is stated rather than inherited.
assert document["ingress"] == {"mode": "direct"}
# Shared with the Docker document, and unchanged by the runtime.
assert document["server"]["host"] == "127.0.0.1"
assert document["storage"]["allowed_host_paths"] == []
assert str(state_dir) in document["store"]["path"]
assert tomllib.loads(launcher.render_config_toml(document)) == document
def test_kubernetes_sandbox_containers_drop_the_docker_capabilities_through_the_template(
kubernetes: None, state_dir: Path
) -> None:
"""The pinned server has no capability field for Kubernetes; its template does it."""
template_file = Path(launcher.config_document()["kubernetes"]["batchsandbox_template_file"])
template = json.loads(template_file.read_text(encoding="utf-8"))
assert template["kind"] == "BatchSandbox"
[container] = template["spec"]["template"]["spec"]["containers"]
# The server picks the container named "sandbox" for its securityContext.
assert container["name"] == "sandbox"
assert container["securityContext"] == {
"capabilities": {"drop": list(launcher.SANDBOX_DROP_CAPABILITIES)}
}
assert "NET_RAW" in launcher.SANDBOX_DROP_CAPABILITIES
def test_the_agent_sandbox_provider_gets_no_template_it_would_not_read(
kubernetes: None, state_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(launcher.KUBE_WORKLOAD_PROVIDER_ENV, "agent-sandbox")
assert "batchsandbox_template_file" not in launcher.config_document()["kubernetes"]
def _replace_on_conflict(template_sc: dict[str, Any], runtime_sc: dict[str, Any]) -> dict[str, Any]:
"""The pinned server's merge: nested dicts recurse, the runtime wins a leaf."""
merged = dict(template_sc)
for key, value in runtime_sc.items():
if isinstance(value, dict) and isinstance(merged.get(key), dict):
merged[key] = _replace_on_conflict(merged[key], value)
else:
merged[key] = value
return merged
def test_the_template_drop_survives_the_network_policy_drop() -> None:
"""A network-policy sandbox's runtime drop must not replace the template's."""
module = ModuleType("opensandbox_server.services.k8s.batchsandbox_provider")
module._merge_security_context = _replace_on_conflict # type: ignore[attr-defined]
template = {"capabilities": {"drop": ["NET_RAW"]}}
runtime = {"capabilities": {"drop": ["NET_ADMIN"], "add": ["SYS_ADMIN"]}, "seccompProfile": {"type": "Unconfined"}}
# What the pinned merge does on its own: the template's drop is gone.
assert module._merge_security_context(template, runtime)["capabilities"]["drop"] == ["NET_ADMIN"]
launcher.drop_template_capabilities_with_the_runtime_ones(module)
merged = module._merge_security_context(template, runtime) # type: ignore[attr-defined]
assert merged == {
"capabilities": {"drop": ["NET_ADMIN", "NET_RAW"], "add": ["SYS_ADMIN"]},
"seccompProfile": {"type": "Unconfined"},
}
# Without a runtime drop the template's stands alone; without a template
# drop the merge is the server's own.
assert module._merge_security_context(template, {})["capabilities"] == {"drop": ["NET_RAW"]} # type: ignore[attr-defined]
assert module._merge_security_context({}, runtime) == _replace_on_conflict({}, runtime) # type: ignore[attr-defined]
def test_the_upstream_merge_keeps_the_template_drop_once_hooked() -> None:
provider = _upstream("opensandbox_server.services.k8s.batchsandbox_provider")
original = provider._merge_security_context
template = {"capabilities": {"drop": ["NET_RAW"]}}
runtime = {"capabilities": {"drop": ["NET_ADMIN"]}}
try:
assert original(template, runtime)["capabilities"]["drop"] == ["NET_ADMIN"]
launcher.drop_template_capabilities_with_the_runtime_ones(provider)
merged = provider._merge_security_context(template, runtime)
finally:
provider._merge_security_context = original
assert merged["capabilities"]["drop"] == ["NET_ADMIN", "NET_RAW"]
def test_docker_refuses_kubernetes_secure_access(
state_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(launcher.SECURE_ACCESS_ENV, "true")
with pytest.raises(
launcher.SandboxServerConfigError,
match="not supported.*Docker",
):
launcher.config_document()
def test_kubernetes_gateway_document_uses_opensandbox_native_ingress(
kubernetes: None, state_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(launcher.INGRESS_MODE_ENV, "gateway")
monkeypatch.setenv(launcher.INGRESS_GATEWAY_ADDRESS_ENV, "gateway.example.com")
ingress = launcher.config_document()["ingress"]
assert ingress == {
"mode": "gateway",
"gateway": {
"address": "gateway.example.com",
"route": {"mode": "uri"},
},
}
assert tomllib.loads(launcher.render_config_toml({"ingress": ingress})) == {"ingress": ingress}
def test_kubernetes_secure_gateway_renders_rotatable_signing_keys(
kubernetes: None, state_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
signing_key = "c2lnbmVkLWVuZHBvaW50LXRlc3Q="
monkeypatch.setenv(launcher.INGRESS_MODE_ENV, "gateway")
monkeypatch.setenv(launcher.INGRESS_GATEWAY_ADDRESS_ENV, "*.sandbox.example.com")
monkeypatch.setenv(launcher.INGRESS_ROUTE_MODE_ENV, "wildcard")
monkeypatch.setenv(launcher.SECURE_ACCESS_ENV, "true")
monkeypatch.setenv(launcher.INGRESS_SIGNING_KEY_ENV, signing_key)
monkeypatch.setenv(launcher.INGRESS_SIGNING_KEY_ID_ENV, "b")
ingress = launcher.config_document()["ingress"]
assert ingress["secure_access"] == {
"active_key": "b",
"keys": [{"key_id": "b", "key": signing_key}],
}
parsed = tomllib.loads(launcher.render_config_toml({"ingress": ingress}))
assert parsed == {"ingress": ingress}
@pytest.mark.parametrize(
("name", "value", "message"),
[
(launcher.INGRESS_MODE_ENV, "unknown", launcher.INGRESS_MODE_ENV),
(launcher.INGRESS_ROUTE_MODE_ENV, "header", "browser-openable"),
(launcher.INGRESS_GATEWAY_ADDRESS_ENV, "https://gateway.example.com", "scheme"),
],
)
def test_kubernetes_gateway_refuses_an_unusable_browser_route(
kubernetes: None,
state_dir: Path,
monkeypatch: pytest.MonkeyPatch,
name: str,
value: str,
message: str,
) -> None:
monkeypatch.setenv(launcher.INGRESS_MODE_ENV, "gateway")
monkeypatch.setenv(launcher.INGRESS_GATEWAY_ADDRESS_ENV, "gateway.example.com")
monkeypatch.setenv(name, value)
with pytest.raises(launcher.SandboxServerConfigError, match=message):
launcher.config_document()
def test_kubernetes_direct_mode_refuses_a_gateway_setting_it_would_ignore(
kubernetes: None, state_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(launcher.INGRESS_GATEWAY_ADDRESS_ENV, "gateway.example.com")
with pytest.raises(
launcher.SandboxServerConfigError, match=launcher.INGRESS_GATEWAY_ADDRESS_ENV
):
launcher.config_document()
def test_kubernetes_direct_mode_reads_an_empty_gateway_setting_as_unset(
kubernetes: None, state_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
# The Kubernetes Compose overlay passes every ingress knob through as
# `${NAME:-}`; a deployment that sets none of them must still start.
for name in (
launcher.INGRESS_MODE_ENV,
launcher.INGRESS_GATEWAY_ADDRESS_ENV,
launcher.INGRESS_ROUTE_MODE_ENV,
launcher.INGRESS_SIGNING_KEY_ENV,
launcher.INGRESS_SIGNING_KEY_ID_ENV,
):
monkeypatch.setenv(name, "")
assert launcher.config_document()["ingress"] == {"mode": "direct"}
def test_kubernetes_gateway_requires_an_address(
kubernetes: None, state_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(launcher.INGRESS_MODE_ENV, "gateway")
with pytest.raises(
launcher.SandboxServerConfigError,
match=launcher.INGRESS_GATEWAY_ADDRESS_ENV,
):
launcher.config_document()
def test_kubernetes_secure_gateway_requires_a_valid_signing_key(
kubernetes: None, state_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(launcher.INGRESS_MODE_ENV, "gateway")
monkeypatch.setenv(launcher.INGRESS_GATEWAY_ADDRESS_ENV, "gateway.example.com")
monkeypatch.setenv(launcher.SECURE_ACCESS_ENV, "true")
with pytest.raises(
launcher.SandboxServerConfigError,
match=launcher.INGRESS_SIGNING_KEY_ENV,
):
launcher.config_document()
monkeypatch.setenv(launcher.INGRESS_SIGNING_KEY_ENV, "not base64!")
with pytest.raises(launcher.SandboxServerConfigError, match="base64"):
launcher.config_document()
def test_kubernetes_document_states_no_hardening_profile_it_cannot_apply(
kubernetes: None, state_dir: Path
) -> None:
"""No capability / privilege / PID keys invented for a block that has none.
Upstream's ``[kubernetes]`` block carries no such field: in Kubernetes the
same layer is the container securityContext, a RuntimeClass and the
kubelet's podPidsLimit. Rendering AstraBox env into fields upstream does not
read would be an inert knob.
"""
flattened = json.dumps(launcher.config_document())
for absent in ("drop_capabilities", "no_new_privileges", "pids_limit", "secure_runtime"):
assert absent not in flattened
def test_kubernetes_document_omits_kubeconfig_path_for_in_cluster_credentials(
kubernetes: None, state_dir: Path
) -> None:
# Upstream spells "use the ServiceAccount" as an ABSENT kubeconfig_path, so
# the key must be missing rather than empty.
assert "kubeconfig_path" not in launcher.config_document()["kubernetes"]
def test_kubernetes_namespace_and_provider_and_policy_are_configurable(
kubernetes: None, state_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(launcher.KUBE_NAMESPACE_ENV, "agents")
monkeypatch.setenv(launcher.KUBE_WORKLOAD_PROVIDER_ENV, "agent-sandbox")
monkeypatch.setenv(launcher.KUBE_IMAGE_PULL_POLICY_ENV, "Always")
monkeypatch.setenv(launcher.KUBE_INFORMER_ENV, "off")
assert launcher.config_document()["kubernetes"] == {
"namespace": "agents",
"workload_provider": "agent-sandbox",
"image_pull_policy": "Always",
"informer_enabled": False,
"sandbox_create_timeout_seconds": 60,
}
@pytest.mark.parametrize(
("env", "value", "match"),
[
(launcher.KUBE_WORKLOAD_PROVIDER_ENV, "batch-sandbox", "batchsandbox"),
(launcher.KUBE_IMAGE_PULL_POLICY_ENV, "ifnotpresent", "IfNotPresent"),
(launcher.KUBE_INFORMER_ENV, "maybe", "boolean"),
],
)
def test_kubernetes_knobs_refuse_a_value_upstream_would_not_check(
kubernetes: None,
state_dir: Path,
monkeypatch: pytest.MonkeyPatch,
env: str,
value: str,
match: str,
) -> None:
"""Each of these reaches a Pod spec unchecked, i.e. fails at CREATE time.
``image_pull_policy`` is typed as a free string upstream and copied into the
container; ``workload_provider`` is looked up in a registry when the app
module is imported. Refusing here is what makes the message name the
variable that was typed.
"""
monkeypatch.setenv(env, value)
with pytest.raises(launcher.SandboxServerConfigError) as error:
launcher.config_document()
assert env in str(error.value)
assert match in str(error.value)
# --- the kubeconfig ---------------------------------------------------------
_KUBECONFIG = """\
apiVersion: v1
kind: Config
clusters:
- name: default
cluster:
server: https://127.0.0.1:6443
certificate-authority-data: Zm9v
contexts:
- name: default
context:
cluster: default
user: default
current-context: default
users:
- name: default
user:
client-certificate-data: YmFy
"""
@pytest.fixture
def kubeconfig(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
path = tmp_path / "kubeconfig"
path.write_text(_KUBECONFIG, encoding="utf-8")
monkeypatch.setenv(launcher.KUBECONFIG_ENV, str(path))
return path
def test_kubeconfig_is_used_verbatim_when_no_address_is_overridden(
kubernetes: None, state_dir: Path, kubeconfig: Path
) -> None:
"""No copy, no parse, no credentials duplicated into the state directory.
AstraBox does NOT detect a loopback address and rewrite it: the address in a
kubeconfig is also the identity TLS verifies, and the container-reachable
name that fixes the routing is normally absent from the API server's
certificate SAN list — so guessing trades a connection error for a
certificate error. The deployment provides one that works.
"""
document = launcher.config_document()
assert document["kubernetes"]["kubeconfig_path"] == str(kubeconfig)
assert not (launcher.metadata_dir().parent / launcher.KUBECONFIG_FILENAME).exists()
assert kubeconfig.read_text(encoding="utf-8") == _KUBECONFIG
def test_kubeconfig_address_override_derives_a_copy_and_leaves_the_source_alone(
kubernetes: None, state_dir: Path, kubeconfig: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
import yaml
monkeypatch.setenv(launcher.KUBE_API_SERVER_ENV, "https://10.0.1.7:6443")
derived = Path(launcher.config_document()["kubernetes"]["kubeconfig_path"])
assert derived == launcher.metadata_dir().parent / launcher.KUBECONFIG_FILENAME
written = yaml.safe_load(derived.read_text(encoding="utf-8"))
assert written["clusters"][0]["cluster"]["server"] == "https://10.0.1.7:6443"
# Everything else rides along untouched — the CA bundle above all, since the
# substituted address has to verify against it.
assert written["clusters"][0]["cluster"]["certificate-authority-data"] == "Zm9v"
assert written["users"][0]["user"]["client-certificate-data"] == "YmFy"
# The operator's file is input, not state.
assert kubeconfig.read_text(encoding="utf-8") == _KUBECONFIG
def test_derived_kubeconfig_absolutises_paths_the_move_would_have_broken(
kubernetes: None, state_dir: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Relative file references are resolved against the SOURCE's directory.
The Kubernetes client resolves ``certificate-authority`` and friends against
the kubeconfig's own directory, so writing the derived copy beside
``server.toml`` would silently re-point every relative name at a sibling of
the copy that does not exist — surfacing as ``File does not exist`` for a
path the operator never configured, on a run whose only change was setting an
address.
"""
import yaml
source_dir = tmp_path / "kube"
source_dir.mkdir()
(source_dir / "ca.crt").write_text("ca", encoding="utf-8")
(source_dir / "client.key").write_text("key", encoding="utf-8")
source = source_dir / "kubeconfig"
source.write_text(
"apiVersion: v1\nkind: Config\n"
"clusters:\n- name: default\n cluster:\n"
" server: https://127.0.0.1:6443\n certificate-authority: ca.crt\n"
"users:\n- name: default\n user:\n client-key: client.key\n",
encoding="utf-8",
)
monkeypatch.setenv(launcher.KUBECONFIG_ENV, str(source))
monkeypatch.setenv(launcher.KUBE_API_SERVER_ENV, "https://10.0.1.7:6443")
derived = Path(launcher.config_document()["kubernetes"]["kubeconfig_path"])
written = yaml.safe_load(derived.read_text(encoding="utf-8"))
assert written["clusters"][0]["cluster"]["certificate-authority"] == str(source_dir / "ca.crt")
assert written["users"][0]["user"]["client-key"] == str(source_dir / "client.key")
def test_derived_kubeconfig_leaves_absolute_paths_alone(
kubernetes: None, state_dir: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
import yaml
source = tmp_path / "kubeconfig"
source.write_text(
"apiVersion: v1\nkind: Config\n"
"clusters:\n- name: default\n cluster:\n"
" server: https://127.0.0.1:6443\n certificate-authority: /etc/ssl/ca.crt\n",
encoding="utf-8",
)
monkeypatch.setenv(launcher.KUBECONFIG_ENV, str(source))
monkeypatch.setenv(launcher.KUBE_API_SERVER_ENV, "https://10.0.1.7:6443")
derived = Path(launcher.config_document()["kubernetes"]["kubeconfig_path"])
written = yaml.safe_load(derived.read_text(encoding="utf-8"))
assert written["clusters"][0]["cluster"]["certificate-authority"] == "/etc/ssl/ca.crt"
@pytest.mark.parametrize("value", ["10.0.0.5:6443", "node.internal:6443", "tcp://x:6443"])
def test_api_server_address_refuses_a_value_with_no_usable_scheme(
kubernetes: None, state_dir: Path, kubeconfig: Path, monkeypatch: pytest.MonkeyPatch, value: str
) -> None:
"""Scheme-less is the natural thing to type and the worst thing to accept.
The Kubernetes client decides whether to load the CA bundle and client
certificate at all by testing ``host.startswith("https")``, so a missing
scheme drops TLS entirely and puts the bearer token on the wire — reaching
the operator as a connection reset with no status code.
"""
monkeypatch.setenv(launcher.KUBE_API_SERVER_ENV, value)
with pytest.raises(launcher.SandboxServerConfigError) as error:
launcher.config_document()
assert launcher.KUBE_API_SERVER_ENV in str(error.value)
assert "scheme" in str(error.value)
def test_api_server_address_without_a_kubeconfig_says_it_reaches_nothing(
kubernetes: None,
state_dir: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""In-cluster credentials have no document to substitute into.
Silence here would leave an operator believing they had redirected the
client at an external API endpoint while it kept talking to
``kubernetes.default.svc``.
"""
monkeypatch.delenv(launcher.KUBECONFIG_ENV, raising=False)
monkeypatch.setenv(launcher.KUBE_API_SERVER_ENV, "https://api.internal:6443")
with caplog.at_level(logging.WARNING):
document = launcher.config_document()
assert "kubeconfig_path" not in document["kubernetes"]
assert launcher.KUBE_API_SERVER_ENV in caplog.text
assert launcher.KUBECONFIG_ENV in caplog.text
def test_a_derived_kubeconfig_nobody_reads_is_removed(
kubernetes: None, state_dir: Path, kubeconfig: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""It holds cluster credentials and lives on the persistent state volume.
Unsetting the address (or switching back to the Docker runtime) leaves it
with no reader at all, and a credential nobody reads still ships in every
backup and image of that volume.
"""
monkeypatch.setenv(launcher.KUBE_API_SERVER_ENV, "https://10.0.1.7:6443")
derived = Path(launcher.config_document()["kubernetes"]["kubeconfig_path"])
assert derived.exists()
monkeypatch.delenv(launcher.KUBE_API_SERVER_ENV)
assert launcher.config_document()["kubernetes"]["kubeconfig_path"] == str(kubeconfig)
assert not derived.exists()
def test_derived_kubeconfig_is_not_readable_by_anyone_else(
kubernetes: None, state_dir: Path, kubeconfig: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""It carries the source's credentials, so it gets the source's protection."""
monkeypatch.setenv(launcher.KUBE_API_SERVER_ENV, "https://10.0.1.7:6443")
derived = Path(launcher.config_document()["kubernetes"]["kubeconfig_path"])
assert derived.stat().st_mode & 0o777 == 0o600
def test_derived_kubeconfig_is_narrowed_even_when_it_already_existed(
kubernetes: None, state_dir: Path, kubeconfig: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
# O_CREAT does not apply a mode to an existing file, so a copy left behind by
# an earlier, wider-umask boot would keep its old permissions forever.
destination = launcher.metadata_dir().parent / launcher.KUBECONFIG_FILENAME
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text("stale", encoding="utf-8")
destination.chmod(0o644)
monkeypatch.setenv(launcher.KUBE_API_SERVER_ENV, "https://10.0.1.7:6443")
launcher.config_document()
assert destination.stat().st_mode & 0o777 == 0o600
def test_kubeconfig_that_cannot_be_read_names_the_user_this_process_runs_as(
kubernetes: None, state_dir: Path, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""The measured failure: a host file mounted 0600 for a different uid.
The server runs as the image's unprivileged user, not as root and not as the
host user who created the file, so ``Permission denied`` here is a mount
problem with a one-line fix — and the message has to say whose uid to make it
readable by.
"""
monkeypatch.setenv(launcher.KUBECONFIG_ENV, str(tmp_path / "absent" / "kubeconfig"))
with pytest.raises(launcher.SandboxServerConfigError) as error:
launcher.config_document()
message = str(error.value)
assert launcher.KUBECONFIG_ENV in message
assert f"uid {os.getuid()}:{os.getgid()}" in message
def test_kubeconfig_address_override_refuses_a_file_it_cannot_apply_to(
kubernetes: None, state_dir: Path, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
# Silently writing a copy with nothing substituted would produce a server
# that dials the original, unreachable address anyway.
empty = tmp_path / "kubeconfig"
empty.write_text("apiVersion: v1\nkind: Config\n", encoding="utf-8")
monkeypatch.setenv(launcher.KUBECONFIG_ENV, str(empty))
monkeypatch.setenv(launcher.KUBE_API_SERVER_ENV, "https://10.0.1.7:6443")
with pytest.raises(launcher.SandboxServerConfigError, match=launcher.KUBE_API_SERVER_ENV):
launcher.config_document()
def test_kubeconfig_address_override_refuses_a_file_that_is_not_yaml(
kubernetes: None, state_dir: Path, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
broken = tmp_path / "kubeconfig"
broken.write_text("clusters: [\n", encoding="utf-8")
monkeypatch.setenv(launcher.KUBECONFIG_ENV, str(broken))
monkeypatch.setenv(launcher.KUBE_API_SERVER_ENV, "https://10.0.1.7:6443")
with pytest.raises(launcher.SandboxServerConfigError, match="not valid YAML"):
launcher.config_document()
# --- the startup proof ------------------------------------------------------
class _ApiError(Exception):
"""The shape the Kubernetes client raises once the API server has answered."""
def __init__(self, status: int) -> None:
super().__init__(f"HTTP {status}")
self.status = status
class _CoreStub:
def __init__(self, *, error: Exception | None = None) -> None:
self.error = error
self.namespaces: list[str] = []
def read_namespace(self, namespace: str) -> object:
self.namespaces.append(namespace)
if self.error is not None:
raise self.error
return object()
class _CustomStub:
def __init__(self, *, error: Exception | None = None) -> None:
self.error = error
self.calls: list[dict[str, Any]] = []
def list_namespaced_custom_object(self, **kwargs: Any) -> object:
self.calls.append(kwargs)
if self.error is not None:
raise self.error
return {"items": []}
class _AuthzStub:
def __init__(
self,
*,
allowed: bool = True,
reason: str | None = None,
evaluation_error: str | None = None,
error: Exception | None = None,
) -> None:
self.allowed = allowed
self.reason = reason
self.evaluation_error = evaluation_error
self.error = error
self.bodies: list[dict[str, Any]] = []
def create_self_subject_access_review(self, body: dict[str, Any]) -> object:
self.bodies.append(body)
if self.error is not None:
raise self.error
return SimpleNamespace(
status=SimpleNamespace(
allowed=self.allowed,
reason=self.reason,
evaluation_error=self.evaluation_error,
)
)
def _verify(
core: _CoreStub | None = None,
custom: _CustomStub | None = None,
authz: _AuthzStub | None = None,
*,
namespace: str = "opensandbox",
provider: str = "batchsandbox",
) -> None:
launcher.verify_kubernetes_access(
core or _CoreStub(),
custom or _CustomStub(),
authz or _AuthzStub(),
api_server="https://10.0.1.7:6443",
namespace=namespace,
workload_provider=provider,
)
def test_startup_proof_asks_about_the_resource_the_provider_actually_creates() -> None:
custom, authz = _CustomStub(), _AuthzStub()
_verify(custom=custom, authz=authz)
assert custom.calls[0]["group"] == "sandbox.opensandbox.io"
assert custom.calls[0]["plural"] == "batchsandboxes"
attributes = authz.bodies[0]["spec"]["resourceAttributes"]
assert attributes == {
"namespace": "opensandbox",
"group": "sandbox.opensandbox.io",
"resource": "batchsandboxes",
"verb": "create",
}
def test_startup_proof_follows_the_configured_provider_to_its_own_resource() -> None:
custom = _CustomStub()
_verify(custom=custom, provider="agent-sandbox")
assert custom.calls[0]["group"] == "agents.x-k8s.io"
assert custom.calls[0]["plural"] == "sandboxes"
def test_missing_namespace_fails_loud_with_the_command_that_fixes_it() -> None:
"""Measured: the first create against a cluster without it 400s.
Upstream addresses a namespace rather than creating one — namespaces are a
cluster-administration boundary — so this is a refusal with the line to run,
not an attempt to create it.
"""
with pytest.raises(launcher.SandboxServerConfigError) as error:
_verify(_CoreStub(error=_ApiError(404)), namespace="sandboxes")
message = str(error.value)
assert "kubectl create namespace sandboxes" in message
assert launcher.KUBE_NAMESPACE_ENV in message
def test_a_namespace_scoped_identity_that_cannot_read_the_namespace_is_not_refused(
caplog: pytest.LogCaptureFixture,
) -> None:
# A ServiceAccount scoped to one namespace legitimately cannot `get` the
# Namespace object; refusing there would reject a working deployment for
# lacking permission it does not need.
with caplog.at_level(logging.WARNING):
_verify(_CoreStub(error=_ApiError(403)))
assert "cannot read the namespace" in caplog.text
@pytest.mark.parametrize("probe", ["namespace", "crd"])
def test_credentials_the_api_server_rejects_outright_are_not_survivable(probe: str) -> None:
"""401 is not 403, and the difference decides whether the server starts.
A 403 is one identity being refused one thing, which the namespace and CRD
probes survive by design. A 401 means nothing authenticated — every later
probe fails the same way, and the access review degrades to a warning when it
cannot ask, so three warnings would add up to a server that starts and can
create nothing.
"""
stubs: dict[str, Any] = {"core": None, "custom": None}
if probe == "namespace":
stubs["core"] = _CoreStub(error=_ApiError(401))
else:
stubs["custom"] = _CustomStub(error=_ApiError(401))
with pytest.raises(launcher.SandboxServerConfigError) as error:
_verify(stubs["core"], stubs["custom"])
assert "rejected these credentials" in str(error.value)
assert launcher.KUBECONFIG_ENV in str(error.value)
def test_a_missing_workload_crd_points_at_the_controller_install() -> None:
with pytest.raises(launcher.SandboxServerConfigError) as error:
_verify(custom=_CustomStub(error=_ApiError(404)))
message = str(error.value)
assert "batchsandboxes.sandbox.opensandbox.io" in message
assert "controller" in message
def test_credentials_that_cannot_create_the_workload_fail_before_serving() -> None:
with pytest.raises(launcher.SandboxServerConfigError) as error:
_verify(authz=_AuthzStub(allowed=False, reason="no RBAC policy matched"))
message = str(error.value)
assert "no RBAC policy matched" in message
assert "kubectl auth can-i create batchsandboxes.sandbox.opensandbox.io" in message
def test_an_authorizer_with_no_opinion_is_a_warning_not_a_refusal(
caplog: pytest.LogCaptureFixture,
) -> None:
# `allowed: false` with an evaluationError means the authorizer could not
# DECIDE, which is not the same answer as "no".
with caplog.at_level(logging.WARNING):
_verify(authz=_AuthzStub(allowed=False, evaluation_error="webhook timeout"))
assert "could not decide" in caplog.text
def test_an_unreachable_api_server_names_both_halves_of_the_address_problem() -> None:
"""A transport failure carries no HTTP status; a TLS failure is one of them.
The measured trap is that the two are one configuration mistake: the address
has to be routable from inside the container AND covered by the API server
certificate's SAN, and the host-gateway name that fixes the first breaks the
second.
"""
with pytest.raises(launcher.SandboxServerConfigError) as error:
_verify(_CoreStub(error=OSError("connection refused")))
message = str(error.value)
assert launcher.KUBE_API_SERVER_ENV in message
assert "SAN" in message
assert "--add-host host.docker.internal:host-gateway" in message
def test_a_tls_identity_failure_lands_on_that_same_answer() -> None:
verify_failed = OSError(
"[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname "
"mismatch, certificate is not valid for 'host.docker.internal'"
)
with pytest.raises(launcher.SandboxServerConfigError, match="certificate"):
_verify(_CoreStub(error=verify_failed))
# --- prepare(): which half of the module runs -------------------------------
@pytest.fixture
def fake_upstream(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> SimpleNamespace:
"""``opensandbox_server``'s shape, enough for :func:`prepare` to complete.
The extra is not installed in the unit lane, and ``prepare`` is where the two
runtimes' halves are chosen — so the choice is nailed here rather than left
to the live lane.
"""
root = ModuleType("opensandbox_server")
config_module = ModuleType("opensandbox_server.config")
config_module.load_config = lambda path: SimpleNamespace( # type: ignore[attr-defined]
log=SimpleNamespace(level="INFO")
)
logging_module = ModuleType("opensandbox_server.logging_config")
logging_module.configure_logging = lambda log: {"version": 1} # type: ignore[attr-defined]
services = ModuleType("opensandbox_server.services")
docker_package = ModuleType("opensandbox_server.services.docker")
upstream_home = tmp_path / "upstream-home"
metadata = _metadata_stub(home=upstream_home)
docker_service, networking, container_ops, constants = _port_assignment_stubs()
volumes = _volumes_stub()
docker_package.metadata = metadata # type: ignore[attr-defined]
docker_package.volumes = volumes # type: ignore[attr-defined]
docker_package.networking = networking # type: ignore[attr-defined]
docker_package.docker_service = docker_service # type: ignore[attr-defined]
docker_package.container_ops = container_ops # type: ignore[attr-defined]
services.constants = constants # type: ignore[attr-defined]
k8s_package = ModuleType("opensandbox_server.services.k8s")
batchsandbox_provider = ModuleType("opensandbox_server.services.k8s.batchsandbox_provider")
batchsandbox_provider._merge_security_context = _replace_on_conflict # type: ignore[attr-defined]
k8s_package.batchsandbox_provider = batchsandbox_provider # type: ignore[attr-defined]
monkeypatch.setattr(launcher, "installation_id", lambda: "this-installation")
monkeypatch.setattr(
launcher, "docker_engine_version", lambda: {"Version": "29.8.1", "ApiVersion": "1.52"}
)
for name, module in {
"opensandbox_server": root,
"opensandbox_server.config": config_module,
"opensandbox_server.logging_config": logging_module,
"opensandbox_server.services": services,
"opensandbox_server.services.constants": constants,
"opensandbox_server.services.docker": docker_package,
"opensandbox_server.services.docker.networking": networking,
"opensandbox_server.services.docker.docker_service": docker_service,
"opensandbox_server.services.docker.container_ops": container_ops,
"opensandbox_server.services.docker.volumes": volumes,
"opensandbox_server.services.k8s": k8s_package,
"opensandbox_server.services.k8s.batchsandbox_provider": batchsandbox_provider,
}.items():
monkeypatch.setitem(sys.modules, name, module)
return SimpleNamespace(
metadata=metadata,
networking=networking,
docker_service=docker_service,
batchsandbox_provider=batchsandbox_provider,
upstream_home=upstream_home,
)
def test_prepare_runs_the_docker_halves_on_the_docker_runtime(
state_dir: Path,
fake_upstream: SimpleNamespace,
monkeypatch: pytest.MonkeyPatch,
) -> None:
pytest.importorskip(
"docker",
reason="the Docker half compares Engine API versions and builds Docker SDK mounts",
)
monkeypatch.setenv(launcher.PUBLISH_HOST_IP_ENV, "172.17.0.1")
launcher.prepare()
assert fake_upstream.docker_service.allocate_port_bindings(
["44772"], min_port=20000, max_port=32000
) == {"44772": ("172.17.0.1", "20000-32000")}
assert _recorded_sidecar_hosts(fake_upstream.networking) == {
"44772": "172.17.0.1",
"8080": "172.17.0.1",
}
networking = object.__new__(fake_upstream.networking.DockerNetworkingMixin)
assert networking._resolve_proxy_host() == "172.17.0.1"
assert networking._resolve_public_host() == "172.17.0.1"
assert fake_upstream.metadata.DEFAULT_STORE_DIR == launcher.metadata_dir()
@pytest.mark.parametrize(
("engine", "api_version"), [("25.0.5", "1.44"), ("20.10.24", "1.41"), ("unknown", "")]
)
def test_prepare_refuses_a_docker_engine_older_than_the_minimum(
state_dir: Path,
fake_upstream: SimpleNamespace,
monkeypatch: pytest.MonkeyPatch,
engine: str,
api_version: str,
) -> None:
pytest.importorskip("docker", reason="the Docker SDK compares Engine API versions")
monkeypatch.setattr(
launcher, "docker_engine_version", lambda: {"Version": engine, "ApiVersion": api_version}
)
with pytest.raises(launcher.SandboxServerConfigError) as refused:
launcher.prepare()
message = str(refused.value)
assert f"Docker Engine {engine}" in message
required = f"Docker Engine {launcher.MIN_DOCKER_ENGINE} (API {launcher.MIN_DOCKER_API_VERSION})"
assert required in message
def test_prepare_accepts_the_minimum_docker_engine(
state_dir: Path, fake_upstream: SimpleNamespace, monkeypatch: pytest.MonkeyPatch
) -> None:
pytest.importorskip("docker", reason="the Docker SDK compares Engine API versions")
monkeypatch.setattr(
launcher, "docker_engine_version", lambda: {"Version": "26.0.0", "ApiVersion": "1.45"}
)
launcher.prepare()
def test_prepare_names_a_daemon_it_cannot_ask(
state_dir: Path, fake_upstream: SimpleNamespace, monkeypatch: pytest.MonkeyPatch
) -> None:
pytest.importorskip("docker", reason="the Docker SDK compares Engine API versions")
def _unreachable() -> dict[str, Any]:
raise ConnectionError("connection refused")
monkeypatch.setattr(launcher, "docker_engine_version", _unreachable)
with pytest.raises(launcher.SandboxServerConfigError, match="connection refused"):
launcher.prepare()
def test_the_installer_requires_the_same_docker_engine() -> None:
"""One minimum: the installer refuses what the server would refuse at start."""
installer = (Path(__file__).resolve().parents[1] / "scripts/install.sh").read_text()
assert f'readonly MIN_DOCKER_ENGINE="{launcher.MIN_DOCKER_ENGINE}"' in installer
assert f'readonly MIN_DOCKER_API="{launcher.MIN_DOCKER_API_VERSION}"' in installer
def test_prepare_skips_the_docker_hooks_on_the_kubernetes_runtime(
kubernetes: None,
state_dir: Path,
fake_upstream: SimpleNamespace,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Skipped BY RUNTIME, not by tripping over the absent ``[docker]`` block.
The publish-host rebind reads ``document["docker"]["port_range_min"]`` for its
proof. A Kubernetes document has no such block, so "skipped" must be a
decision — a KeyError caught somewhere would be the same outcome for the
wrong reason, and would break the moment the document grew that key.
"""
seen: list[tuple[Path | None, str]] = []
def _no_docker() -> dict[str, Any]:
raise AssertionError("the Kubernetes runtime drives no Docker daemon")
monkeypatch.setattr(launcher, "docker_engine_version", _no_docker)
monkeypatch.setattr(
launcher,
"kubernetes_preflight",
lambda kubeconfig, document: seen.append((kubeconfig, document["kubernetes"]["namespace"])),
)
launcher.prepare()
assert seen == [(None, launcher.DEFAULT_KUBE_NAMESPACE)]
# The Kubernetes half: the template's NET_RAW drop survives the runtime's.
merged = fake_upstream.batchsandbox_provider._merge_security_context(
{"capabilities": {"drop": ["NET_RAW"]}}, {"capabilities": {"drop": ["NET_ADMIN"]}}
)
assert merged["capabilities"]["drop"] == ["NET_ADMIN", "NET_RAW"]
# Untouched: nothing publishes a host port, and a renewed lease is written to
# the workload's own spec.expireTime rather than to a local directory.
assert fake_upstream.docker_service.allocate_port_bindings(["44772"]) == {
"44772": ("0.0.0.0", 40000)
}
assert _recorded_sidecar_hosts(fake_upstream.networking) == {
"44772": "0.0.0.0",
"8080": "0.0.0.0",
}
networking = object.__new__(fake_upstream.networking.DockerNetworkingMixin)
assert networking._resolve_proxy_host() == "127.0.0.1"
assert networking._resolve_public_host() == "127.0.0.1"
assert fake_upstream.metadata.DEFAULT_STORE_DIR == fake_upstream.upstream_home
def test_prepare_hands_the_preflight_the_kubeconfig_the_server_will_open(
kubernetes: None,
state_dir: Path,
kubeconfig: Path,
fake_upstream: SimpleNamespace,
monkeypatch: pytest.MonkeyPatch,
) -> None:
# The proof is worthless if it dials credentials the server will not use.
monkeypatch.setenv(launcher.KUBE_API_SERVER_ENV, "https://10.0.1.7:6443")
seen: list[Path | None] = []
monkeypatch.setattr(
launcher, "kubernetes_preflight", lambda kubeconfig, document: seen.append(kubeconfig)
)
launcher.prepare()
assert seen == [launcher.metadata_dir().parent / launcher.KUBECONFIG_FILENAME]
def test_prepare_does_not_warn_about_metadata_persistence_under_kubernetes(
kubernetes: None,
state_dir: Path,
fake_upstream: SimpleNamespace,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""That warning's consequence does not exist on this runtime.
It is about losing the only durable record of a lease RENEWAL, which is a
Docker-runtime fact (Docker cannot update a running container's labels).
Kubernetes renewals are written to the workload's spec, in the cluster.
"""
monkeypatch.setattr(launcher, "metadata_dir", lambda: Path("/tmp/astrabox-k8s/metadata"))
monkeypatch.setattr(launcher, "kubernetes_preflight", lambda kubeconfig, document: None)
with caplog.at_level(logging.WARNING):
launcher.prepare()
assert "will probably NOT survive a restart" not in caplog.text
@pytest.mark.parametrize(
("runtime", "env", "value"),
[
(launcher.RUNTIME_KUBERNETES, launcher.PORT_RANGE_ENV, "40000-60000"),
(launcher.RUNTIME_DOCKER, launcher.KUBE_NAMESPACE_ENV, "agents"),
],
)
def test_a_knob_the_other_runtime_owns_is_reported_rather_than_ignored(
caplog: pytest.LogCaptureFixture,
monkeypatch: pytest.MonkeyPatch,
runtime: str,
env: str,
value: str,
) -> None:
"""The "set it, nothing happens" knob class, closed in both directions."""
monkeypatch.setenv(env, value)
with caplog.at_level(logging.WARNING):
launcher.warn_about_inert_knobs(runtime)
assert env in caplog.text
assert "other sandbox runtime" in caplog.text
def test_nothing_is_reported_when_only_the_running_runtimes_knobs_are_set(
caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(launcher.KUBE_NAMESPACE_ENV, "agents")
with caplog.at_level(logging.WARNING):
launcher.warn_about_inert_knobs(launcher.RUNTIME_KUBERNETES)
assert caplog.text == ""
# --- the REAL upstream package (skipped when the extra is absent) -----------
def test_upstream_still_resolves_a_direct_endpoint_to_the_pod_ip() -> None:
"""Pin the direct-mode endpoint shape exposed by the upstream package.
``[ingress] mode = "direct"`` answers with the workload's own
``<pod IP>:<port>``. That is dialable from a container that shares the node's
Pod-CIDR route and not from one that does not. Multi-node deployments use
gateway mode instead.
"""
provider = _upstream("opensandbox_server.services.k8s.batchsandbox_provider")
instance = object.__new__(provider.BatchSandboxProvider)
instance.ingress_config = SimpleNamespace(mode="direct")
workload = {
"metadata": {
"annotations": {
"sandbox.opensandbox.io/endpoints": '["10.42.0.17"]',
}
}
}
endpoint = instance.get_endpoint_info(workload, 44772, "sandbox-id")
assert endpoint is not None
assert endpoint.endpoint == "10.42.0.17:44772"
assert instance.get_endpoint_info({}, 44772, "sandbox-id") is None
def test_upstream_still_registers_the_workload_resources_this_module_names() -> None:
"""The startup proof asks about a group/plural AstraBox holds a copy of.
It has to: the question is asked before upstream's provider is constructed.
A rename upstream must therefore fail here, not at the first create.
"""
factory = _upstream("opensandbox_server.services.k8s.provider_factory")
client = _upstream("opensandbox_server.services.k8s.client")
assert set(factory._PROVIDER_REGISTRY) == set(launcher.WORKLOAD_RESOURCES)
group, version, _ = launcher.WORKLOAD_RESOURCES["batchsandbox"]
assert (group, version) == (client.OPENSANDBOX_API_GROUP, client.OPENSANDBOX_API_VERSION)
def test_upstream_still_creates_no_namespace_of_its_own() -> None:
"""The reason the namespace precheck refuses instead of creating one."""
service = _upstream("opensandbox_server.services.k8s.kubernetes_service")
assert not hasattr(service.KubernetesSandboxService, "create_namespace")
client = _upstream("opensandbox_server.services.k8s.client")
assert not hasattr(client.K8sClient, "create_namespace")
def test_upstream_config_accepts_the_kubernetes_document_astrabox_renders(
kubernetes: None, state_dir: Path, tmp_path: Path
) -> None:
config = _upstream("opensandbox_server.config")
document = launcher.config_document()
path = tmp_path / "k8s.toml"
path.write_text(launcher.render_config_toml(document), encoding="utf-8")
parsed = config.load_config(path)
assert parsed.runtime.type == "kubernetes"
assert parsed.kubernetes.namespace == launcher.DEFAULT_KUBE_NAMESPACE
assert parsed.kubernetes.workload_provider == launcher.DEFAULT_KUBE_WORKLOAD_PROVIDER
assert parsed.ingress.mode == "direct"
def test_upstream_config_accepts_the_secure_gateway_document_astrabox_renders(
kubernetes: None,
state_dir: Path,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
config = _upstream("opensandbox_server.config")
signing_key = "c2lnbmVkLWVuZHBvaW50LXRlc3Q="
monkeypatch.setenv(launcher.INGRESS_MODE_ENV, "gateway")
monkeypatch.setenv(launcher.INGRESS_GATEWAY_ADDRESS_ENV, "gateway.example.com")
monkeypatch.setenv(launcher.SECURE_ACCESS_ENV, "true")
monkeypatch.setenv(launcher.INGRESS_SIGNING_KEY_ENV, signing_key)
document = launcher.config_document()
path = tmp_path / "k8s-secure.toml"
path.write_text(launcher.render_config_toml(document), encoding="utf-8")
parsed = config.load_config(path)
assert parsed.ingress.mode == "gateway"
assert parsed.ingress.gateway.address == "gateway.example.com"
assert parsed.ingress.gateway.route.mode == "uri"
assert parsed.ingress.secure_access.active_key == "a"
assert parsed.ingress.secure_access.keys[0].key == signing_key
# ── [secure_runtime]: the hardening every sandbox runs under ─────────────────
def test_no_hardened_runtime_is_configured_by_default(state_dir: Path) -> None:
"""Absent means runc — and absent from the DOCUMENT, not present and empty.
Upstream validates the block when it is there, so emitting an empty one
would turn "this deployment did not choose" into a configuration the server
has to accept or reject.
"""
assert launcher.secure_runtime() == {}
assert "secure_runtime" not in launcher.config_document()
@pytest.mark.parametrize(
("chosen", "docker_runtime"),
[("gvisor", "runsc"), ("kata", "kata-runtime"), ("firecracker", "firecracker")],
)
def test_a_hardened_runtime_is_named_to_both_substrates(
chosen: str, docker_runtime: str, state_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""One choice, two names: Docker takes a runtime, Kubernetes a RuntimeClass.
The conventional RuntimeClass name is the type itself, while Docker's
runtime name is not (`gvisor` is installed as `runsc`), which is why
upstream keeps two fields rather than one.
"""
monkeypatch.setenv(launcher.SECURE_RUNTIME_ENV, chosen)
block = launcher.secure_runtime()
assert block == {
"type": chosen,
"docker_runtime": docker_runtime,
"k8s_runtime_class": chosen,
}
assert launcher.config_document()["secure_runtime"] == block
def test_the_choice_is_case_and_space_insensitive(
state_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(launcher.SECURE_RUNTIME_ENV, " GVisor ")
assert launcher.secure_runtime()["type"] == "gvisor"
def test_an_unknown_runtime_refuses_to_start(
state_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The whole value of this knob is that a wrong one cannot run unhardened.
A typo that fell through to runc would leave a deployment believing it was
isolated when it was not — the one failure this must never have.
"""
monkeypatch.setenv(launcher.SECURE_RUNTIME_ENV, "gvisr")
with pytest.raises(launcher.SandboxServerConfigError) as caught:
launcher.secure_runtime()
assert "gvisr" in str(caught.value)
assert "gvisor" in str(caught.value)
def test_upstream_accepts_the_hardened_document(
kubernetes: None, state_dir: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Against upstream's own loader, so the block is one it really reads."""
monkeypatch.setenv(launcher.SECURE_RUNTIME_ENV, "gvisor")
config = _upstream("opensandbox_server.config")
path = tmp_path / "hardened.toml"
path.write_text(launcher.render_config_toml(launcher.config_document()), encoding="utf-8")
parsed = config.load_config(path)
assert parsed.secure_runtime.type == "gvisor"
assert parsed.secure_runtime.k8s_runtime_class == "gvisor"
# ── [egress]: the sidecar a network policy would be enforced by ──────────────
def test_the_pinned_egress_sidecar_is_available_by_default(state_dir: Path) -> None:
assert launcher.egress() == {
"image": launcher.DEFAULT_EGRESS_IMAGE,
"mode": "dns+nft",
"disable_ipv6": True,
}
assert launcher.config_document()["egress"]["image"] == launcher.DEFAULT_EGRESS_IMAGE
def test_naming_the_sidecar_renders_the_block(
state_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(launcher.EGRESS_IMAGE_ENV, launcher.DEFAULT_EGRESS_IMAGE)
assert launcher.egress() == {
"image": launcher.DEFAULT_EGRESS_IMAGE,
"mode": "dns+nft",
"disable_ipv6": True,
}
assert launcher.config_document()["egress"]["image"] == launcher.DEFAULT_EGRESS_IMAGE
def test_the_strict_mode_is_the_default(state_dir: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""`dns+nft`, deliberately not upstream's own `dns` default."""
monkeypatch.setenv(launcher.EGRESS_IMAGE_ENV, "example/egress:1")
assert launcher.egress()["mode"] == "dns+nft"
def test_the_name_only_mode_is_refused(state_dir: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""`dns` enforces no IP or CIDR rule, and every policy AstraBox writes has them.
Under it the cloud-metadata deny, the Docker bridge deny and the address
entries of a Limited allow list reach no packet, so a sandbox connecting to
an address directly would pass all three.
"""
monkeypatch.setenv(launcher.EGRESS_IMAGE_ENV, "example/egress:1")
monkeypatch.setenv(launcher.EGRESS_MODE_ENV, "dns")
with pytest.raises(launcher.SandboxServerConfigError) as caught:
launcher.egress()
assert "no IP or CIDR rule" in str(caught.value)
def test_ipv6_is_disabled_in_every_sandbox_namespace(
kubernetes: None, state_dir: Path, tmp_path: Path
) -> None:
"""Written, not inherited: upstream's own default could change under a pin bump."""
config = _upstream("opensandbox_server.config")
path = tmp_path / "ipv6.toml"
path.write_text(launcher.render_config_toml(launcher.config_document()), encoding="utf-8")
assert "disable_ipv6 = true" in path.read_text(encoding="utf-8")
assert config.load_config(path).egress.disable_ipv6 is True
def test_an_unknown_egress_mode_refuses_to_start(
state_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(launcher.EGRESS_IMAGE_ENV, "example/egress:1")
monkeypatch.setenv(launcher.EGRESS_MODE_ENV, "nft")
with pytest.raises(launcher.SandboxServerConfigError) as caught:
launcher.egress()
assert "nft" in str(caught.value)
def test_the_mode_is_refused_even_with_no_sidecar_named(
state_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A bad value must not wait for someone to also set the image to be noticed.
Validating only on the path that uses it would let a typo sit in a
deployment's environment until the day it turns egress on — which is the day
it least wants to discover one.
"""
monkeypatch.setenv(launcher.EGRESS_MODE_ENV, "strict")
with pytest.raises(launcher.SandboxServerConfigError):
launcher.egress()
def test_upstream_accepts_the_egress_document(
kubernetes: None, state_dir: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(launcher.EGRESS_IMAGE_ENV, launcher.DEFAULT_EGRESS_IMAGE)
config = _upstream("opensandbox_server.config")
path = tmp_path / "egress.toml"
path.write_text(launcher.render_config_toml(launcher.config_document()), encoding="utf-8")
parsed = config.load_config(path)
assert parsed.egress.image == launcher.DEFAULT_EGRESS_IMAGE
assert parsed.egress.mode == "dns+nft"
def test_create_timeout_defaults_to_upstreams_own_sixty() -> None:
"""The default is upstream's, restated rather than invented.
Sixty seconds is also the symptom: it is what a create gives up at on a
cold node, so a reader who finds this value has found the knob they came
for.
"""
assert launcher.kube_create_timeout_seconds() == 60
@pytest.mark.parametrize("value", ["0", "-1", "60s", "1.5"])
def test_create_timeout_refuses_what_upstream_would_reject(
monkeypatch: pytest.MonkeyPatch, value: str
) -> None:
"""Refused here, where the message can name the variable.
Upstream constrains the field to ``>= 1``. Passing a bad value through
would move the rejection to the far side of the seam, where it describes
upstream's schema to someone who typed an AstraBox variable.
"""
monkeypatch.setenv(launcher.KUBE_CREATE_TIMEOUT_ENV, value)
with pytest.raises(launcher.SandboxServerConfigError):
launcher.kube_create_timeout_seconds()
def test_upstream_accepts_the_create_timeout(
kubernetes: None, state_dir: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The field reaches upstream under the name upstream reads.
A rendered key upstream ignores is an inert knob that looks configured, and
nothing else in this suite would notice: the TOML would be well-formed and
the sandbox would go on timing out at sixty. Parsing the document with
upstream's own loader is what makes the wiring real rather than plausible.
"""
monkeypatch.setenv(launcher.KUBE_CREATE_TIMEOUT_ENV, "420")
config = _upstream("opensandbox_server.config")
path = tmp_path / "kubernetes.toml"
path.write_text(launcher.render_config_toml(launcher.config_document()), encoding="utf-8")
parsed = config.load_config(path)
assert parsed.kubernetes.sandbox_create_timeout_seconds == 420