Files
50230616d5 refactor(runtime): retire Community image dependencies (#3386)
* feat(sandbox): default to official Alpine sandbox image

default_sandbox_image() now returns docker.io/library/alpine:3.22, a generic
version-qualified official image, so a fresh install no longer depends on the
community sandbox image catalog. All compute drivers (docker, podman,
kubernetes, vm) inherit this fallback.

Part of #3116.

Signed-off-by: Akram
Signed-off-by: Akram <akram.benaissi@gmail.com>

* feat(deploy): default deployment configs to the official Alpine sandbox image

Update the shared gateway default_image, Helm chart values, the standalone
Kubernetes manifest, and the dev gateway task scripts to use
docker.io/library/alpine:3.22 instead of the community base image, consistent
with default_sandbox_image(). GPU e2e image-build base is left unchanged (CUDA
needs a glibc base).

Part of #3116.

Signed-off-by: Akram
Signed-off-by: Akram <akram.benaissi@gmail.com>

* feat(driver): default to numeric non-root identity for USER-less images

With the default sandbox image now Alpine, images that declare no OCI USER
must start instead of being rejected. When the image declares no USER and
the policy requests none, the Podman and Docker drivers now supply a numeric
non-root identity (DEFAULT_SANDBOX_UID/GID = 1000) instead of rejecting,
matching the numeric-identity behavior of the Kubernetes and VM drivers. The
supervisor's resolved-identity path runs the sandbox as a synthesized
non-root account without the account existing in the image. Images that
declare a USER keep the OCI resolution path unchanged.

Part of #3116.

Signed-off-by: Akram <akram.benaissi@gmail.com>
Signed-off-by: Evan Lezar <elezar@nvidia.com>

* test(conformance): use Alpine workload image

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* refactor(policy): drop community image /app path from default policy

The restrictive default policy granted read-only access to /app, a directory
that only existed in the community base image. A generic Alpine default has no
/app, so remove it. Landlock best-effort already ignores absent paths; this
just stops advertising a community-specific layout in the default.

Part of #3116.

Signed-off-by: Akram
Signed-off-by: Akram <akram.benaissi@gmail.com>

* docs(config): document Alpine default images

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* fix(podman): report early sandbox termination

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* fix(podman): initialize rootless workspace ownership

Signed-off-by: Evan Lezar <elezar@nvidia.com>

* fix(sandbox): qualify NVIDIA Ubuntu default

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(podman): initialize rootful default workspace

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(sftp): add native sandbox adapter

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sftp): gate runtime helper support to Linux

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sftp): support standard OpenSSH file operations

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(sftp): harden rename and special file handling

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(runtime): remove community image dependencies

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* test(e2e): build provider readiness tool fixture

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(e2e): use a dedicated Noble fixture for Docker tests

Signed-off-by: Evan Lezar <elezar@nvidia.com>

---------

Signed-off-by: Akram
Signed-off-by: Akram <akram.benaissi@gmail.com>
Signed-off-by: Evan Lezar <elezar@nvidia.com>
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
Co-authored-by: Evan Lezar <elezar@nvidia.com>
Co-authored-by: Drew Newberry <anewberry@nvidia.com>
2026-09-22 14:43:51 +02:00

127 lines
4.2 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import fcntl
import os
import time
from typing import TYPE_CHECKING
import grpc
import pytest
from openshell import Sandbox, SandboxClient, WorkspaceClient
if TYPE_CHECKING:
from collections.abc import Callable, Iterator
def pytest_configure(config: pytest.Config) -> None:
config.addinivalue_line(
"markers",
"exclusive_gateway_config: hold an exclusive lock on gateway-global "
"config so no other xdist worker observes a transient global setting",
)
@pytest.fixture(autouse=True)
def _gateway_config_guard(
request: pytest.FixtureRequest,
tmp_path_factory: pytest.TempPathFactory,
) -> Iterator[None]:
"""Readers-writer guard over gateway-global config mutations.
The e2e gateway is shared across xdist workers, so a test that flips a
gateway-global setting can leak that transient value into another worker's
sandbox creation. Every test holds a shared lock by default; a test marked
``exclusive_gateway_config`` holds an exclusive lock, so while it mutates and
restores the setting no other worker is mid-test and none can observe the
transient value. The lock file lives in the run's shared base temp dir
(``getbasetemp().parent``), which is common to all xdist workers.
"""
lock_path = tmp_path_factory.getbasetemp().parent / "gateway-config.lock"
exclusive = (
request.node.get_closest_marker("exclusive_gateway_config") is not None
)
with lock_path.open("w") as lock_file:
fcntl.flock(lock_file, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
yield
@pytest.fixture(scope="session")
def cluster_name() -> str | None:
return os.environ.get("OPENSHELL_GATEWAY")
@pytest.fixture(scope="session")
def sandbox_client(cluster_name: str | None) -> Iterator[SandboxClient]:
with SandboxClient.from_active_cluster(cluster=cluster_name) as client:
yield client
@pytest.fixture(scope="session", autouse=True)
def ensure_sandbox_persistence_ready(sandbox_client: SandboxClient) -> None:
for _ in range(60):
try:
sandbox_client.list_ids(workspace="default", page_size=1)
return
except grpc.RpcError as exc:
details = exc.details() or ""
if exc.code() == grpc.StatusCode.UNAVAILABLE:
time.sleep(2)
continue
if (
exc.code() == grpc.StatusCode.INTERNAL
and "no such table: objects" in details
):
time.sleep(1)
continue
raise
pytest.fail(
"openshell-server persistence is not initialized (missing sqlite objects table); "
"redeploy the active cluster and rerun e2e sandbox tests"
)
@pytest.fixture
def sandbox(cluster_name: str | None) -> Callable[..., Sandbox]:
def _create(*, spec: object | None = None, delete_on_exit: bool = True) -> Sandbox:
return Sandbox(
workspace="default",
cluster=cluster_name,
spec=spec,
delete_on_exit=delete_on_exit,
# Allow time to pull an explicitly supplied workload fixture.
ready_timeout_seconds=300.0,
)
return _create
@pytest.fixture(scope="session")
def workspace_client(sandbox_client: SandboxClient) -> WorkspaceClient:
return WorkspaceClient.from_sandbox_client(sandbox_client)
@pytest.fixture(scope="session")
def _worker_suffix(worker_id: str) -> str:
"""Return a suffix for worker-unique resource names.
Uses the built-in ``worker_id`` fixture from pytest-xdist which returns
``"gw0"``, ``"gw1"``, etc. for workers, or ``"master"`` for non-xdist runs.
"""
if worker_id == "master":
return ""
return f"-{worker_id}"
@pytest.fixture
def run_python() -> Callable[[Sandbox, str], tuple[int, str, str]]:
def _run(sandbox: Sandbox, code: str) -> tuple[int, str, str]:
result = sandbox.exec(["python", "-c", code], timeout_seconds=20)
return result.exit_code, result.stdout, result.stderr
return _run