Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 24 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,36 @@ jobs:

- name: Install SDK dependencies
run: |
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
python -m pip install -e './sdk/python[all]'

- name: Run unit tests
run: |
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
make sdk-test

sdk-quality:
name: Python SDK lint and type checks
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.10"
cache: pip
cache-dependency-path: sdk/python/pyproject.toml

- name: Install SDK development dependencies
run: |
python -m pip install -e './sdk/python[dev]'

- name: Run SDK quality checks
run: |
make sdk-check

deployment-script-syntax:
name: Deployment script syntax
runs-on: ubuntu-latest
Expand All @@ -65,7 +87,6 @@ jobs:

- name: Check deployment script syntax
run: |
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
make deploy-script-check

standalone-e2e:
Expand All @@ -81,7 +102,6 @@ jobs:

- name: Initialize build submodules
run: |
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
git submodule update --init src/sandboxd src/distill-fs

- name: Set up Python
Expand All @@ -93,20 +113,17 @@ jobs:

- name: Install SDK dependencies
run: |
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
python -m pip install -e './sdk/python[all]'

- name: Prepare host kernel modules
run: |
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
sudo modprobe loop
sudo modprobe erofs
sudo modprobe br_netfilter
sudo sysctl -w net.bridge.bridge-nf-call-iptables=1

- name: Build all-in-one image
run: |
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
AKERNEL_ENABLE_KATA=false \
make build \
IMAGE_REPOSITORY=akernel-ci/all-in-one \
Expand All @@ -115,14 +132,12 @@ jobs:

- name: Start standalone AKernel
run: |
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
IMAGE="akernel-ci/all-in-one:${GITHUB_SHA}" \
AKERNEL_NAT_BACKEND=iptables \
./deploy/standalone/start.sh

- name: Run SDK end-to-end examples
run: |
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
gateway_ip="$(docker inspect \
--format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
akernel-traefik)"
Expand Down Expand Up @@ -151,7 +166,6 @@ jobs:
- name: Show standalone diagnostics
if: failure()
run: |
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
docker ps -a
for container in akernel-node akernel-traefik; do
if docker container inspect "${container}" >/dev/null 2>&1; then
Expand All @@ -168,7 +182,6 @@ jobs:
- name: Stop standalone AKernel
if: always()
run: |
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
if [[ -x ./deploy/standalone/stop.sh ]]; then
./deploy/standalone/stop.sh
else
Expand Down
6 changes: 0 additions & 6 deletions .github/workflows/release-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ jobs:
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY

if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Release tag must use the stable vX.Y.Z format: ${RELEASE_TAG}" >&2
exit 1
Expand Down Expand Up @@ -68,18 +66,15 @@ jobs:

- name: Install build dependencies
run: |
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
python -m pip install --upgrade pip
python -m pip install -e './sdk/python[dev]' 'twine>=5,<7'

- name: Check SDK
run: |
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
make sdk-check

- name: Build distributions
run: |
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
rm -rf sdk/python/dist
python -m build --outdir sdk/python/dist sdk/python
test "$(find sdk/python/dist -maxdepth 1 -name '*.whl' | wc -l)" -eq 1
Expand All @@ -88,7 +83,6 @@ jobs:

- name: Test installed wheel
run: |
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY
python -m venv /tmp/akernel-wheel-test
/tmp/akernel-wheel-test/bin/python -m pip install \
--no-deps sdk/python/dist/*.whl
Expand Down
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ project guidance.
AKernel provides cluster-backed remote sandbox environments for agents and
developer workflows. The current public user-facing surface is the Python
`akernel-sdk`, including the `akernel_sdk.Sandbox` API and the `ak` CLI.
The default sandbox runtime is gVisor runsc; callers may select Kata
Containers when the cluster has a KVM-capable node. Creation-time network
policies support unrestricted networking, blocking new flows except the
YuanRong control and published sandbox-port routes, or denying exact and
leading-wildcard DNS names.
The default sandbox runtime is gVisor runsc. Runtime identifiers are forwarded
to the selected backend, which owns availability and compatibility checks; the
bundled deployment also advertises Kata Containers on KVM-capable nodes.
Creation-time network policies support unrestricted networking, blocking new
flows except the YuanRong control and published sandbox-port routes, or denying
exact and leading-wildcard DNS names.
Experimental whole-device NVIDIA GPU and configurable writable-storage
requests currently require runsc.

Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ sdk-test:

.PHONY: sdk-check
sdk-check: sdk-test
@cd sdk/python; \
@set -e; \
cd sdk/python; \
python3 -m ruff check akernel_sdk tests; \
python3 -m mypy akernel_sdk

Expand Down
22 changes: 15 additions & 7 deletions sdk/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,9 @@ with Sandbox(xpu="gpu:l20:1") as sandbox:

The `type:model:count` value is case-insensitive and canonicalized to lower
case. The model is required and matched exactly; wildcard models are not
supported. GPU sandboxes currently require the gVisor `runsc` runtime and a
node configured for gVisor nvproxy.
supported. The bundled backend currently requires the gVisor `runsc` runtime
and a node configured for gVisor nvproxy. Runtime compatibility is validated
by the backend rather than the SDK.

Set the writable root filesystem quota in MiB:

Expand All @@ -125,9 +126,10 @@ with Sandbox(storage_mb=20 * 1024) as sandbox:
print(sandbox.commands.run("df -h /").stdout)
```

An explicit `storage_mb` quota currently requires `runsc` and uses sandboxd's
disk-backed XFS filestore. When it is omitted, sandboxd retains its configured
default 10 GiB memory-backed writable overlay. See
The bundled backend currently requires `runsc` for an explicit `storage_mb`
quota and uses sandboxd's disk-backed XFS filestore. Runtime compatibility is
validated by the backend. When `storage_mb` is omitted, sandboxd retains its
configured default 10 GiB memory-backed writable overlay. See
[`examples/gpu_sandbox.py`](./examples/gpu_sandbox.py) and
[`examples/storage_sandbox.py`](./examples/storage_sandbox.py).

Expand Down Expand Up @@ -195,15 +197,21 @@ configuration, as described in the

## Sandbox runtimes

AKernel uses the gVisor `runsc` runtime when `runtime` is omitted. Callers may also select `runsc` explicitly or request Kata Containers:
AKernel uses the gVisor `runsc` runtime when `runtime` is omitted. Runtime
identifiers are forwarded to the selected backend instead of being restricted
by an SDK-owned registry. The bundled deployment advertises `runsc` and Kata
Containers:

```python
default_sandbox = Sandbox()
runsc_sandbox = Sandbox(runtime="runsc")
kata_sandbox = Sandbox(runtime="kata")
```

Kata requires at least one cluster node whose sandboxd instance successfully initialized the Kata runtime with a usable `/dev/kvm` device. Nodes without KVM remain available for runsc workloads and do not advertise Kata; when no eligible Kata node exists, the scheduler returns a no-resource error.
Kata requires at least one cluster node whose sandboxd instance successfully
initialized the Kata runtime with a usable `/dev/kvm` device. Nodes without
KVM remain available for runsc workloads and do not advertise Kata. A runtime
that is unavailable in the cluster fails scheduling or backend validation.

See [`examples/sandbox_runtime.py`](./examples/sandbox_runtime.py) for a runnable example.

Expand Down
5 changes: 2 additions & 3 deletions sdk/python/akernel_sdk/_backends/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from collections.abc import Mapping
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Literal, Protocol
from typing import Protocol

from .._addresses import Endpoint
from ..types import (
Expand All @@ -37,7 +37,6 @@
class Capability(Enum):
"""Features whose availability differs between backends."""

KATA_RUNTIME = auto()
S3_ROOTFS = auto()
NODE_PLACEMENT = auto()
CUSTOM_REVERSE_TUNNEL_PORTS = auto()
Expand All @@ -59,7 +58,7 @@ class SandboxSpec:

image: str | None
rootfs: S3Config | None
runtime: Literal["runsc", "kata"]
runtime: str
cpu: int
memory: int
cpu_limit: int
Expand Down
1 change: 0 additions & 1 deletion sdk/python/akernel_sdk/_backends/openyuanrong_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,6 @@ class OpenYuanRongSandboxBackend:
namespace = _NAMESPACE
capabilities = frozenset(
{
Capability.KATA_RUNTIME,
Capability.S3_ROOTFS,
Capability.NODE_PLACEMENT,
}
Expand Down
4 changes: 0 additions & 4 deletions sdk/python/akernel_sdk/_backends/openyuanrong_sdk_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,6 @@ def build_options(
raise ValueError("mem_limit must be 0 or greater than or equal to memory")
normalized_xpu = normalize_xpu(xpu)
validate_storage_mb(storage_mb)
if normalized_xpu is not None and runtime != "runsc":
raise ValueError("xpu is currently supported only by runsc")
if storage_mb is not None and runtime != "runsc":
raise ValueError("storage_mb is currently supported only by runsc")

options = yr.InvokeOptions()
# A Sandbox is driven by one sequential SDK client. Disabling ordered RPC
Expand Down
30 changes: 13 additions & 17 deletions sdk/python/akernel_sdk/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import urllib.request
from collections.abc import Mapping, Sequence
from types import MappingProxyType
from typing import Literal, cast

from ._addresses import Endpoint, api_endpoint_from_env, gateway_endpoint_from_env
from ._backends.base import BackendSession, SandboxSpec
Expand All @@ -33,7 +32,6 @@
from .pty import Pty
from .types import HttpReverseTunnel, Mount, NetworkPolicy, S3Config, SandboxInfo

_SUPPORTED_RUNTIMES = ("runsc", "kata")
_traefik_internal_ip_cache: str | None = None
logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -108,8 +106,8 @@ def _get_traefik_internal_ip(gateway: Endpoint) -> tuple[str, int]:
class Sandbox:
"""A remote AKernel sandbox.

The public API is backend-neutral. AKernel supports the gVisor ``runsc``
runtime and Kata Containers on KVM-capable nodes.
The public API is backend-neutral. The selected backend and cluster
determine which sandbox runtime identifiers are available.
"""

def __init__(
Expand Down Expand Up @@ -141,7 +139,8 @@ def __init__(
Args:
image: OCI image used as the sandbox root filesystem.
rootfs: S3-compatible EROFS root filesystem configuration.
runtime: Sandbox runtime name: ``runsc`` or ``kata``.
runtime: Sandbox runtime identifier. Defaults to ``runsc``;
availability is determined by the backend and cluster.
cpu: Requested CPU in millicores.
memory: Requested memory in MiB.
cpu_limit: CPU limit in millicores, or zero to follow ``cpu``.
Expand All @@ -158,10 +157,11 @@ def __init__(
node_id: Require placement on a specific AKernel node.
xpu: Experimental whole-device accelerator request in
``type:model:count`` format. Currently only exact-model NVIDIA
GPU requests with the ``runsc`` runtime are supported.
GPU requests are supported. The backend validates runtime
compatibility.
storage_mb: Experimental writable root filesystem quota in MiB.
When omitted, the configured default is used. Explicit quotas
currently require the ``runsc`` runtime.
are validated against the selected runtime by the backend.
network_policy: Optional creation-time network policy. Omitting it
leaves sandbox networking unrestricted.

Expand All @@ -177,21 +177,17 @@ def __init__(
raise TypeError("rootfs must be an S3Config")
if image is not None and rootfs is not None:
raise ValueError("image and rootfs are mutually exclusive")
if runtime not in _SUPPORTED_RUNTIMES:
raise ValueError(
f"unsupported runtime {runtime!r}; "
f"supported runtimes: {', '.join(_SUPPORTED_RUNTIMES)}"
)
if not isinstance(runtime, str):
raise TypeError("runtime must be a string")
runtime = runtime.strip()
if not runtime:
raise ValueError("runtime must be a non-empty string")
normalized_xpu = normalize_xpu(xpu)
validate_storage_mb(storage_mb)
if network_policy is not None and not isinstance(
network_policy, NetworkPolicy
):
raise TypeError("network_policy must be a NetworkPolicy or None")
if normalized_xpu is not None and runtime != "runsc":
raise ValueError("xpu is currently supported only by runsc")
if storage_mb is not None and runtime != "runsc":
raise ValueError("storage_mb is currently supported only by runsc")
_validate_integer("cpu", cpu, minimum=1)
_validate_integer("memory", memory, minimum=1)
_validate_integer("cpu_limit", cpu_limit, minimum=0)
Expand Down Expand Up @@ -255,7 +251,7 @@ def __init__(
spec = SandboxSpec(
image=image,
rootfs=rootfs,
runtime=cast(Literal["runsc", "kata"], runtime),
runtime=runtime,
cpu=cpu,
memory=memory,
cpu_limit=cpu_limit,
Expand Down
1 change: 0 additions & 1 deletion sdk/python/benchmarks/sandbox_pressure.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,6 @@ def _percentiles(label, samples_s):
)
parser.add_argument(
"--runtime",
choices=("runsc", "kata"),
default=DEFAULT_RUNTIME,
help="sandbox runtime (default: AKERNEL_PRESSURE_RUNTIME or runsc)",
)
Expand Down
4 changes: 3 additions & 1 deletion sdk/python/tests/unit/test_addresses.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ def test_explicit_server_port_uses_plain_http_gateway(self):
gateway_expected = ("http", "10.0.0.1", 8888, False)
self.assertEqual(endpoint_tuple(api_endpoint_from_env()), expected)
self.assertEqual(endpoint_tuple(exec_endpoint_from_env()), expected)
self.assertEqual(endpoint_tuple(gateway_endpoint_from_env()), gateway_expected)
self.assertEqual(
endpoint_tuple(gateway_endpoint_from_env()), gateway_expected
)

def test_gateway_override_defaults_to_plain_http(self):
with patch.dict(
Expand Down
8 changes: 4 additions & 4 deletions sdk/python/tests/unit/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,20 +147,20 @@ def test_connection_config_maps_to_yr_environment(self):
self.assertEqual(os.environ["YR_GATEWAY_TLS"], "0")
self.assertEqual(os.environ["YR_TOKEN"], "secret")

def test_kata_without_explicit_rootfs_passes_runtime_config_override(self):
def test_runtime_identifier_without_explicit_rootfs_is_forwarded(self):
native = MagicMock()
native.id = "default-kata"
native.id = "default-gvisor-next"
with patch.object(
openyuanrong_sandbox.yr_sandbox,
"Sandbox",
return_value=native,
) as sandbox_type:
self.backend.create(_spec(runtime="kata"))
self.backend.create(_spec(runtime="gvisor-next"))

# YuanRong applies this runtime as a configuration override to the
# deployed default rootfs; the adapter does not build a filesystem
# overlay.
self.assertEqual(sandbox_type.call_args.kwargs["runtime"], "kata")
self.assertEqual(sandbox_type.call_args.kwargs["runtime"], "gvisor-next")
self.assertIsNone(sandbox_type.call_args.kwargs["rootfs"])

def test_runsc_without_explicit_rootfs_passes_runtime_config_override(self):
Expand Down
Loading