mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-20 06:00:06 +08:00
* feat(isolation): add RFC 0012 backend contract Signed-off-by: Drew Newberry <385+drew@users.noreply.github.com> * refactor(isolation): name the interface crate explicitly Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): expose trusted host gateway Signed-off-by: Drew Newberry <anewberry@nvidia.com> * docs(agents): inventory the MXC driver Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): add mediated DNS transport Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): tighten interface error and digest contracts Signed-off-by: Drew Newberry <anewberry@nvidia.com> * docs(isolation): remove unrelated driver inventory Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): define capability-free launch contract Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): seal confirmed boundary state Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): validate confirmation for external backend implementations Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): clarify mediated DNS identity Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(isolation): generalize loopback connector Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(isolation): unify typed network mediation Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): bind launches to sandbox sessions Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(mxc): initialize extended sandbox status Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): add boundary protocol and Linux primitives Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): harden signals and separate process status from transport Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): validate remote confirmation through public contract Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): validate wire state and propagate snapshot failures Signed-off-by: Drew Newberry <anewberry@nvidia.com> * test(isolation): import owned agent specification explicitly Signed-off-by: Drew Newberry <anewberry@nvidia.com> * docs(isolation): describe mediated DNS channel Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): bound mediation attach without nested retries Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(isolation): generalize loopback protocol Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): add transport-neutral session authentication Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(isolation): separate sandbox backend protocol Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): harden runtime boundary controls Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): add terminal boundary operation Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): split supervisor and sandbox runtimes Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): harden boundary isolation and lifecycle ownership Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): reject private root redirects and adopt typed errors Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): preserve accept thread ownership on musl Signed-off-by: Drew Newberry <anewberry@nvidia.com> * test(sandbox): isolate credential probes from filtered threads Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): return retained exec exit status to independent waiters Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): bound network mediation and preserve socket authorization Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): bound control admission and retire stale mediation Signed-off-by: Drew Newberry <anewberry@nvidia.com> * ci(e2e): select migrated drivers per stack layer Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(sandbox): implement loopback connector Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(isolation): authenticate the Sandbox Protocol Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(supervisor): rotate launch-scoped authentication Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(sandbox): consume dedicated backend crate Signed-off-by: Drew Newberry <anewberry@nvidia.com> * test(sandbox): align topology session fixture Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): align projected bootstrap bundle Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(auth): validate refreshed credentials before rotation Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): fail closed across supervisor disconnects Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): repair rebased sandbox CI Signed-off-by: Drew Newberry <anewberry@nvidia.com> * build(runtime): publish separate sandbox and supervisor images Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(config): configure the sandbox runtime image Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(ci): validate sandbox binary linkage Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(isolation): use backend and runtime terminology Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(sandbox): use a scratch runtime image Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(ci): refresh schema and dependency policy Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(sandbox): bind reconnects to supervisor process Signed-off-by: Drew Newberry <anewberry@nvidia.com> * docs: align runtime split operational guidance Signed-off-by: Drew Newberry <anewberry@nvidia.com> * chore(security): document Kubernetes runtime RBAC Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): enforce runtime lifecycle invariants Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(compute): identify sandbox start generations Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(server): restore sandbox launch sessions Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): support authenticated runtime replacement Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(auth): bind sandbox session successors Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(auth): retry pending sandbox successors Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(vm): run the supervisor outside the guest workload Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(vm): use sandbox backend protocol Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): repair rebase integration Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): use unified build toolchain Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(vm): use sandbox runtime terminology Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(vm): own guest network bootstrap Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): expose guest init version Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): select native supervisor artifacts Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): guard guest init Linux symbols Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): scope Linux test imports Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): avoid guest interface casts Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): reconcile admitted sandbox identity Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): share resolved sandbox identity Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): surface host supervisor failures Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): include guest logs on supervisor exit Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): rotate and clean runtime generations Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): make sandbox starts generation-aware Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): rotate restored sandbox sessions Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(vm): keep shared paths in the base layer Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): bind sandbox session lineage Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(docker): isolate workloads behind the host supervisor Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(docker): rotate launch-scoped authentication Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(docker): use sandbox backend protocol Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(docker): use host networking for supervisor Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): preserve host gateway alias resolution Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(docker): use separate sandbox and supervisor images Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): restore startup validation after rebase Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(docker): name the sandbox runtime directly Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): narrow supervisor CA runtime storage Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): close companion isolation gaps Signed-off-by: Drew Newberry <anewberry@nvidia.com> * test(docker): align mediated network expectations Signed-off-by: Drew Newberry <anewberry@nvidia.com> * test(docker): exercise mediated network paths Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): attach supervisor to managed network Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): defer supervisor recovery until gateway is ready Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): make sandbox starts generation-aware Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): rotate restored sandbox sessions Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): preserve workloads during session rotation Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(docker): remove unrelated configuration RFC changes Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): bind sandbox session lineage Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(kubernetes): add proxy-pod isolation topology Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(kubernetes): use sandbox backend protocol Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): use stable sandbox service authority Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(kubernetes): split sandbox and supervisor images Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): adapt proxy pods to current runtime APIs Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(kubernetes): describe the single runtime placement Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(kubernetes): simplify sandbox orchestration Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): validate deployment prerequisites Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): update Trivy Helm profile inventory Signed-off-by: Drew Newberry <anewberry@nvidia.com> * test(kubernetes): update Trivy scan inventory count Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): reuse preloaded runtime images in e2e Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): type and clean runtime resources Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): make sandbox restarts recoverable Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): rotate restored sandbox sessions Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): preserve supervisor egress Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): bind sandbox session lineage Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(podman): adopt isolated sandbox and supervisor containers Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): stage bootstrap archives at named volume destinations Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(podman): rotate launch-scoped authentication Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(podman): use sandbox backend protocol Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(podman): use host networking for supervisor Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(podman): split sandbox and supervisor images Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): repair rebase integration Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(podman): name the sandbox runtime directly Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): provision supervisor CA runtime storage Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): address isolation review findings Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): inspect Debian supervisor provenance Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): use libpod-compatible tmpfs options Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): bind verified sandbox runtime binary Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): provide external driver data directory Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): start sandbox before joining user namespace Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): separate supervisor user namespace Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): make sandbox starts generation-aware Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): rotate restored sandbox sessions Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): bind sandbox session lineage Signed-off-by: Drew Newberry <anewberry@nvidia.com> * perf(isolation): add TCP and DNS benchmark harnesses Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(perf): align benchmark timing and supported protocols Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(perf): report TCP benchmark metrics accurately Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(perf): cancel failed worker startup Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(docker): build matching local supervisor image Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(podman): make local sandbox smoke test runnable Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): wire local sandbox runtime image Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(kubernetes): narrow sandbox service RBAC Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(ci): validate split runtime artifacts Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): harden runtime session handling Signed-off-by: Drew Newberry <anewberry@nvidia.com> * feat(supervisor): add standalone network proxy role Signed-off-by: Drew Newberry <anewberry@nvidia.com> * docs(rfc): remove implementation companion notes Signed-off-by: Drew Newberry <anewberry@nvidia.com> * refactor(vm): standardize runtime release name Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(vm): pin renamed runtime artifacts Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(auth): persist sandbox runtime identity Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(runtime): restore branch validation Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(isolation): reconcile main after rebase Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(network): close unframed HTTP 1.0 responses Signed-off-by: Drew Newberry <anewberry@nvidia.com> * chore(isolation): preserve upstream OCSF updates Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(security): close credential and TLS replay paths Signed-off-by: Drew Newberry <anewberry@nvidia.com> * fix(auth): make sandbox refresh retries idempotent Signed-off-by: Drew Newberry <anewberry@nvidia.com> --------- Signed-off-by: Drew Newberry <385+drew@users.noreply.github.com> Signed-off-by: Drew Newberry <anewberry@nvidia.com>
538 lines
17 KiB
Rust
538 lines
17 KiB
Rust
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
#![cfg(feature = "e2e-local-container-driver")]
|
|
|
|
use std::fs;
|
|
use std::io::Write;
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
use bollard::Docker;
|
|
use bollard::models::{ContainerCreateBody, HostConfig, Mount, MountTypeEnum, VolumeCreateRequest};
|
|
use bollard::query_parameters::{
|
|
CreateContainerOptionsBuilder, CreateImageOptionsBuilder, LogsOptions, RemoveContainerOptions,
|
|
RemoveVolumeOptionsBuilder, StartContainerOptions, WaitContainerOptions,
|
|
};
|
|
use futures_util::TryStreamExt;
|
|
use openshell_e2e::harness::container::{ImageGuard, e2e_driver};
|
|
use openshell_e2e::harness::sandbox::SandboxGuard;
|
|
use serde_json::{Map, Value};
|
|
|
|
const TEST_IMAGE: &str = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest";
|
|
const VOLUME_TARGET: &str = "/sandbox/e2e-volume";
|
|
const BIND_TARGET: &str = "/sandbox/e2e-bind";
|
|
#[cfg(feature = "e2e-docker")]
|
|
const OCI_VOLUME_TARGET: &str = "/workspace/project/e2e-volume";
|
|
#[cfg(feature = "e2e-docker")]
|
|
const OCI_USER_DOCKERFILE: &str = r#"FROM public.ecr.aws/docker/library/python:3.13-slim
|
|
|
|
RUN apt-get update && apt-get install -y --no-install-recommends iproute2 \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
WORKDIR /workspace/project
|
|
RUN chown 2234:2235 .
|
|
USER 2234:2235
|
|
CMD ["sleep", "infinity"]
|
|
"#;
|
|
|
|
static NEXT_VOLUME_ID: AtomicU64 = AtomicU64::new(0);
|
|
|
|
struct VolumeGuard {
|
|
docker: Docker,
|
|
name: String,
|
|
}
|
|
|
|
impl VolumeGuard {
|
|
async fn create(driver: &str) -> Result<Self, String> {
|
|
let name = unique_volume_name(driver);
|
|
let docker = connect_container_api(driver).await?;
|
|
docker
|
|
.create_volume(VolumeCreateRequest {
|
|
name: Some(name.clone()),
|
|
..Default::default()
|
|
})
|
|
.await
|
|
.map_err(|err| format!("create {driver} volume {name}: {err}"))?;
|
|
Ok(Self { docker, name })
|
|
}
|
|
}
|
|
|
|
impl Drop for VolumeGuard {
|
|
fn drop(&mut self) {
|
|
let docker = self.docker.clone();
|
|
let name = self.name.clone();
|
|
tokio::spawn(async move {
|
|
let _ = docker
|
|
.remove_volume(
|
|
&name,
|
|
Some(RemoveVolumeOptionsBuilder::new().force(true).build()),
|
|
)
|
|
.await;
|
|
});
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn sandbox_mounts_existing_driver_config_volume() {
|
|
let driver = e2e_driver().expect("OPENSHELL_E2E_DRIVER must be set by the e2e wrapper");
|
|
assert!(
|
|
matches!(driver.as_str(), "docker" | "podman"),
|
|
"driver_config volume e2e requires docker or podman, got {driver}"
|
|
);
|
|
|
|
let volume = VolumeGuard::create(&driver)
|
|
.await
|
|
.expect("create named test volume");
|
|
|
|
seed_volume(&volume).await.expect("seed named test volume");
|
|
|
|
let driver_config = format!(
|
|
r#"{{"{driver}":{{"mounts":[{{"type":"volume","source":"{}","target":"{VOLUME_TARGET}","read_only":false}}]}}}}"#,
|
|
volume.name
|
|
);
|
|
let mut sandbox = SandboxGuard::create(&[
|
|
"--driver-config-json",
|
|
&driver_config,
|
|
"--",
|
|
"sh",
|
|
"-lc",
|
|
"set -eu; test \"$(cat /sandbox/e2e-volume/input.txt)\" = host-volume-ok; printf sandbox-volume-ok > /sandbox/e2e-volume/output.txt; cat /sandbox/e2e-volume/output.txt",
|
|
])
|
|
.await
|
|
.expect("sandbox create with driver-config volume");
|
|
|
|
assert!(
|
|
sandbox.create_output.contains("sandbox-volume-ok"),
|
|
"sandbox should read and write the mounted volume:\n{}",
|
|
sandbox.create_output
|
|
);
|
|
|
|
sandbox.cleanup().await;
|
|
verify_volume(&volume)
|
|
.await
|
|
.expect("verify sandbox wrote to named test volume");
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "e2e-docker")]
|
|
async fn oci_workspace_preparation_skips_nested_volume_ownership() {
|
|
let driver = e2e_driver().expect("OPENSHELL_E2E_DRIVER must be set by the e2e wrapper");
|
|
assert!(
|
|
driver == "docker",
|
|
"OCI workspace mount e2e requires docker, got {driver}"
|
|
);
|
|
|
|
let volume = VolumeGuard::create(&driver)
|
|
.await
|
|
.expect("create named test volume");
|
|
seed_volume(&volume).await.expect("seed named test volume");
|
|
|
|
let image_context = tempfile::tempdir().expect("create OCI image context");
|
|
let dockerfile = image_context.path().join("Dockerfile");
|
|
fs::write(&dockerfile, OCI_USER_DOCKERFILE).expect("write OCI image Dockerfile");
|
|
let image = ImageGuard::build(
|
|
&format!("{driver}-oci-user"),
|
|
&dockerfile,
|
|
image_context.path(),
|
|
)
|
|
.expect("build OCI-user image with selected container engine");
|
|
|
|
let driver_config = format!(
|
|
r#"{{"{driver}":{{"mounts":[{{"type":"volume","source":"{}","target":"{OCI_VOLUME_TARGET}","read_only":false}}]}}}}"#,
|
|
volume.name
|
|
);
|
|
let mut sandbox = SandboxGuard::create_keep_with_args(
|
|
&[
|
|
"--from",
|
|
image.tag(),
|
|
"--driver-config-json",
|
|
&driver_config,
|
|
"--no-tty",
|
|
],
|
|
&[
|
|
"sh",
|
|
"-lc",
|
|
"set -eu; test \"$(id -u):$(id -g)\" = 2234:2235; \
|
|
test \"$(pwd -P)\" = /workspace/project; test \"$HOME\" = /workspace/project; \
|
|
test \"$(stat -c %u:%g /workspace/project/e2e-volume/input.txt)\" = 0:0; \
|
|
touch direct-write; echo Ready; sleep infinity",
|
|
],
|
|
"Ready",
|
|
)
|
|
.await
|
|
.expect("create OCI-user sandbox with nested volume");
|
|
|
|
let ssh_output = sandbox
|
|
.exec(&[
|
|
"sh",
|
|
"-lc",
|
|
"set -eu; test \"$(pwd -P)\" = /workspace/project; \
|
|
test \"$HOME\" = /workspace/project; \
|
|
test \"$(stat -c %u:%g /workspace/project/e2e-volume/input.txt)\" = 0:0; \
|
|
touch ssh-write; echo nested-mount-owner-ok",
|
|
])
|
|
.await
|
|
.expect("SSH child should preserve nested volume ownership");
|
|
assert!(
|
|
ssh_output.contains("nested-mount-owner-ok"),
|
|
"expected nested mount ownership marker:\n{ssh_output}"
|
|
);
|
|
|
|
sandbox.cleanup().await;
|
|
verify_volume_ownership(&volume)
|
|
.await
|
|
.expect("nested volume ownership should remain unchanged");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn sandbox_mounts_enabled_driver_config_bind() {
|
|
let driver = e2e_driver().expect("OPENSHELL_E2E_DRIVER must be set by the e2e wrapper");
|
|
assert!(
|
|
matches!(driver.as_str(), "docker" | "podman"),
|
|
"driver_config bind e2e requires docker or podman, got {driver}"
|
|
);
|
|
|
|
let cwd = std::env::current_dir().expect("resolve current dir");
|
|
let host_dir = tempfile::Builder::new()
|
|
.prefix("openshell-e2e-driver-config-bind-")
|
|
.tempdir_in(cwd)
|
|
.expect("create bind mount host dir");
|
|
fs::set_permissions(host_dir.path(), fs::Permissions::from_mode(0o777))
|
|
.expect("make bind mount host dir writable by sandbox user");
|
|
let input_path = host_dir.path().join("input.txt");
|
|
fs::write(&input_path, "host-bind-ok").expect("seed bind mount host dir");
|
|
fs::set_permissions(&input_path, fs::Permissions::from_mode(0o666))
|
|
.expect("make bind mount input readable by sandbox user");
|
|
|
|
let bind_source = bind_mount_source_path(&driver, host_dir.path());
|
|
let bind_mount = serde_json::json!({
|
|
"type": "bind",
|
|
"source": bind_source,
|
|
"target": BIND_TARGET,
|
|
"read_only": false,
|
|
"selinux_label": "private"
|
|
});
|
|
let driver_config = driver_config_mount_json(&driver, &bind_mount);
|
|
// Host bind mounts are explicitly unsafe: this test validates driver mount
|
|
// wiring, not Landlock enforcement over Docker Desktop's fakeowner mounts.
|
|
let policy = write_bind_mount_policy().expect("write bind mount policy");
|
|
let policy_path = policy.path().to_str().expect("policy path must be utf-8");
|
|
let mut sandbox = SandboxGuard::create(&[
|
|
"--policy",
|
|
policy_path,
|
|
"--driver-config-json",
|
|
&driver_config,
|
|
"--",
|
|
"sh",
|
|
"-lc",
|
|
"set -eu; test \"$(cat /sandbox/e2e-bind/input.txt)\" = host-bind-ok; printf sandbox-bind-ok > /sandbox/e2e-bind/output.txt; cat /sandbox/e2e-bind/output.txt",
|
|
])
|
|
.await
|
|
.expect("sandbox create with driver-config bind mount");
|
|
|
|
assert!(
|
|
sandbox.create_output.contains("sandbox-bind-ok"),
|
|
"sandbox should read and write the bind mount:\n{}",
|
|
sandbox.create_output
|
|
);
|
|
|
|
sandbox.cleanup().await;
|
|
let output = fs::read_to_string(host_dir.path().join("output.txt"))
|
|
.expect("read sandbox output from bind mount host dir");
|
|
assert_eq!(output, "sandbox-bind-ok");
|
|
}
|
|
|
|
fn write_bind_mount_policy() -> Result<tempfile::NamedTempFile, String> {
|
|
let mut file =
|
|
tempfile::NamedTempFile::new().map_err(|err| format!("create bind policy: {err}"))?;
|
|
file.write_all(
|
|
br#"version: 1
|
|
|
|
filesystem_policy:
|
|
include_workdir: false
|
|
read_only:
|
|
- "/bin"
|
|
- "/dev"
|
|
- "/etc"
|
|
- "/lib"
|
|
- "/proc"
|
|
- "/usr"
|
|
read_write:
|
|
- "/sandbox/e2e-bind"
|
|
- "/tmp"
|
|
|
|
landlock:
|
|
compatibility: best_effort
|
|
|
|
process:
|
|
run_as_user: sandbox
|
|
run_as_group: sandbox
|
|
"#,
|
|
)
|
|
.map_err(|err| format!("write bind policy: {err}"))?;
|
|
Ok(file)
|
|
}
|
|
|
|
async fn seed_volume(volume: &VolumeGuard) -> Result<(), String> {
|
|
run_volume_container(
|
|
volume,
|
|
"seed",
|
|
false,
|
|
"set -eu; chmod 0777 /vol; printf host-volume-ok > /vol/input.txt",
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn verify_volume(volume: &VolumeGuard) -> Result<(), String> {
|
|
let output = run_volume_container(
|
|
volume,
|
|
"verify",
|
|
true,
|
|
"set -eu; test \"$(cat /vol/input.txt)\" = host-volume-ok; test \"$(cat /vol/output.txt)\" = sandbox-volume-ok; echo volume-ok",
|
|
)
|
|
.await?;
|
|
if !output.contains("volume-ok") {
|
|
return Err(format!(
|
|
"volume verification did not print expected marker:\n{output}"
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "e2e-docker")]
|
|
async fn verify_volume_ownership(volume: &VolumeGuard) -> Result<(), String> {
|
|
let output = run_volume_container(
|
|
volume,
|
|
"verify-owner",
|
|
true,
|
|
"set -eu; test \"$(stat -c %u:%g /vol/input.txt)\" = 0:0; echo owner-ok",
|
|
)
|
|
.await?;
|
|
if !output.contains("owner-ok") {
|
|
return Err(format!(
|
|
"volume ownership verification did not print expected marker:\n{output}"
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn run_volume_container(
|
|
volume: &VolumeGuard,
|
|
purpose: &str,
|
|
read_only: bool,
|
|
script: &str,
|
|
) -> Result<String, String> {
|
|
ensure_test_image(&volume.docker).await?;
|
|
|
|
let container_name = format!("{}-{purpose}", volume.name);
|
|
let create_options = CreateContainerOptionsBuilder::new()
|
|
.name(&container_name)
|
|
.build();
|
|
let host_config = HostConfig {
|
|
mounts: Some(vec![Mount {
|
|
target: Some("/vol".to_string()),
|
|
source: Some(volume.name.clone()),
|
|
typ: Some(MountTypeEnum::VOLUME),
|
|
read_only: Some(read_only),
|
|
..Default::default()
|
|
}]),
|
|
..Default::default()
|
|
};
|
|
volume
|
|
.docker
|
|
.create_container(
|
|
Some(create_options),
|
|
ContainerCreateBody {
|
|
image: Some(TEST_IMAGE.to_string()),
|
|
user: Some("0:0".to_string()),
|
|
entrypoint: Some(vec!["sh".to_string()]),
|
|
cmd: Some(vec!["-lc".to_string(), script.to_string()]),
|
|
attach_stdout: Some(true),
|
|
attach_stderr: Some(true),
|
|
host_config: Some(host_config),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await
|
|
.map_err(|err| format!("create helper container {container_name}: {err}"))?;
|
|
|
|
let result = run_created_container(volume, &container_name).await;
|
|
let remove_result = volume
|
|
.docker
|
|
.remove_container(&container_name, None::<RemoveContainerOptions>)
|
|
.await;
|
|
|
|
match (result, remove_result) {
|
|
(Ok(output), Ok(())) => Ok(output),
|
|
(Ok(_), Err(err)) => Err(format!("remove helper container {container_name}: {err}")),
|
|
(Err(err), Ok(())) => Err(err),
|
|
(Err(err), Err(remove_err)) => Err(format!(
|
|
"{err}\nremove helper container {container_name}: {remove_err}"
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn ensure_test_image(docker: &Docker) -> Result<(), String> {
|
|
if docker.inspect_image(TEST_IMAGE).await.is_ok() {
|
|
return Ok(());
|
|
}
|
|
|
|
let pull_events = docker
|
|
.create_image(
|
|
Some(
|
|
CreateImageOptionsBuilder::new()
|
|
.from_image(TEST_IMAGE)
|
|
.build(),
|
|
),
|
|
None,
|
|
None,
|
|
)
|
|
.try_collect::<Vec<_>>()
|
|
.await
|
|
.map_err(|err| format!("pull helper image {TEST_IMAGE}: {err}"))?;
|
|
|
|
let pull_errors = pull_events
|
|
.iter()
|
|
.filter_map(|event| {
|
|
event
|
|
.error_detail
|
|
.as_ref()
|
|
.and_then(|detail| detail.message.as_deref())
|
|
})
|
|
.collect::<Vec<_>>();
|
|
if pull_errors.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
Err(format!(
|
|
"pull helper image {TEST_IMAGE} failed:\n{}",
|
|
pull_errors.join("\n")
|
|
))
|
|
}
|
|
|
|
async fn run_created_container(
|
|
volume: &VolumeGuard,
|
|
container_name: &str,
|
|
) -> Result<String, String> {
|
|
volume
|
|
.docker
|
|
.start_container(container_name, None::<StartContainerOptions>)
|
|
.await
|
|
.map_err(|err| format!("start helper container {container_name}: {err}"))?;
|
|
|
|
let wait_result = volume
|
|
.docker
|
|
.wait_container(container_name, None::<WaitContainerOptions>)
|
|
.try_collect::<Vec<_>>()
|
|
.await;
|
|
let logs = volume
|
|
.docker
|
|
.logs(
|
|
container_name,
|
|
Some(LogsOptions {
|
|
stdout: true,
|
|
stderr: true,
|
|
tail: "all".to_string(),
|
|
..Default::default()
|
|
}),
|
|
)
|
|
.try_collect::<Vec<_>>()
|
|
.await
|
|
.map(|chunks| {
|
|
chunks
|
|
.into_iter()
|
|
.map(|chunk| chunk.to_string())
|
|
.collect::<String>()
|
|
});
|
|
|
|
match (wait_result, logs) {
|
|
(Ok(_), Ok(output)) => Ok(output),
|
|
(Ok(_), Err(err)) => Err(format!(
|
|
"read helper container {container_name} logs: {err}"
|
|
)),
|
|
(Err(err), Ok(output)) => Err(format!(
|
|
"helper container {container_name} failed: {err}\n{output}"
|
|
)),
|
|
(Err(err), Err(log_err)) => Err(format!(
|
|
"helper container {container_name} failed: {err}\nread logs failed: {log_err}"
|
|
)),
|
|
}
|
|
}
|
|
|
|
async fn connect_container_api(driver: &str) -> Result<Docker, String> {
|
|
let docker = match driver {
|
|
"docker" => Docker::connect_with_local_defaults()
|
|
.map_err(|err| format!("connect to Docker API: {err}"))?,
|
|
"podman" => {
|
|
let socket = podman_socket_path()?;
|
|
let socket_display = socket.display().to_string();
|
|
Docker::connect_with_unix(
|
|
socket
|
|
.to_str()
|
|
.ok_or_else(|| format!("podman socket path is not UTF-8: {socket_display}"))?,
|
|
120,
|
|
bollard::API_DEFAULT_VERSION,
|
|
)
|
|
.map_err(|err| format!("connect to Podman Docker-compatible API: {err}"))?
|
|
}
|
|
other => return Err(format!("unsupported e2e driver for volume API: {other}")),
|
|
};
|
|
docker
|
|
.ping()
|
|
.await
|
|
.map_err(|err| format!("ping {driver} Docker-compatible API: {err}"))?;
|
|
Ok(docker)
|
|
}
|
|
|
|
fn podman_socket_path() -> Result<PathBuf, String> {
|
|
let path = std::env::var_os("OPENSHELL_PODMAN_SOCKET").ok_or_else(|| {
|
|
"OPENSHELL_PODMAN_SOCKET must be set by e2e/with-podman-gateway.sh".to_string()
|
|
})?;
|
|
Ok(PathBuf::from(path))
|
|
}
|
|
|
|
fn unique_volume_name(driver: &str) -> String {
|
|
let nanos = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.expect("system clock should be after Unix epoch")
|
|
.as_nanos();
|
|
let sequence = NEXT_VOLUME_ID.fetch_add(1, Ordering::Relaxed);
|
|
format!(
|
|
"openshell-e2e-driver-config-volume-{driver}-{}-{nanos}-{sequence}",
|
|
std::process::id()
|
|
)
|
|
}
|
|
|
|
fn driver_config_mount_json(driver: &str, mount: &Value) -> String {
|
|
let mut root = Map::new();
|
|
root.insert(
|
|
driver.to_string(),
|
|
serde_json::json!({
|
|
"mounts": [mount]
|
|
}),
|
|
);
|
|
Value::Object(root).to_string()
|
|
}
|
|
|
|
fn bind_mount_source_path(driver: &str, path: &Path) -> PathBuf {
|
|
if driver == "docker" {
|
|
github_actions_host_work_path(path).unwrap_or_else(|| path.to_path_buf())
|
|
} else {
|
|
path.to_path_buf()
|
|
}
|
|
}
|
|
|
|
fn github_actions_host_work_path(path: &Path) -> Option<PathBuf> {
|
|
if std::env::var("GITHUB_ACTIONS").ok().as_deref() != Some("true") {
|
|
return None;
|
|
}
|
|
|
|
let relative = path.strip_prefix("/__w").ok()?;
|
|
let mapped = Path::new("/home/runner/_work").join(relative);
|
|
mapped.exists().then_some(mapped)
|
|
}
|