mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-16 05:20:22 +08:00
feat(vm): fall back to Podman socket when Docker is unavailable (#1370)
* test(e2e): add bypass detection test for sandbox REJECT rules Validates that direct TCP connections bypassing the HTTP CONNECT proxy are rejected immediately (ECONNREFUSED) rather than hanging until a network timeout. This is implementation-agnostic — it tests observable kernel behavior regardless of whether rules are installed via iptables or nftables. * feat(vm): fall back to Podman socket when Docker is unavailable The VM driver resolves sandbox images from a local container engine before falling back to registry pulls. Previously it only checked Docker. Now it tries the Podman socket as a fallback, since Podman exposes a Docker-compatible API via bollard. On Linux, enable the Podman API socket with: systemctl --user start podman.socket Signed-off-by: Russell Bryant <russell.bryant@gmail.com> * chore(vm): fix formatting Signed-off-by: Russell Bryant <russell.bryant@gmail.com> * refactor(vm): rename docker-specific identifiers to engine-neutral names Rename resolve_local_docker_image → resolve_local_container_image, local_docker_image_platform_mismatch → local_image_platform_mismatch, and the local docker variable → engine since the function now supports both Docker and Podman backends. Signed-off-by: Russell Bryant <russell.bryant@gmail.com> --------- Signed-off-by: Russell Bryant <russell.bryant@gmail.com>
This commit is contained in:
@@ -166,9 +166,12 @@ The VM guest's serial console is appended to `<state-dir>/<sandbox-id>/console.l
|
||||
- Matching rustup target: `rustup target add aarch64-unknown-linux-gnu` (or `x86_64-unknown-linux-gnu` for an amd64 guest)
|
||||
- `cargo install --locked cargo-zigbuild` and `brew install zig` (or distro equivalent). `vm:supervisor` uses `cargo zigbuild` to cross-compile the in-VM `openshell-sandbox` supervisor binary.
|
||||
- [mise](https://mise.jdx.dev/) task runner
|
||||
- Docker-compatible socket on the local CLI/gateway host when using
|
||||
- Docker or Podman socket on the local CLI/gateway host when using
|
||||
`openshell sandbox create --from ./Dockerfile` or `--from ./dir`; the CLI
|
||||
builds the image and the VM driver exports it via the local Docker daemon
|
||||
builds the image and the VM driver exports it via the local container engine.
|
||||
Docker is tried first; if unavailable, the driver falls back to the Podman
|
||||
socket. On Linux, enable the Podman API socket with
|
||||
`systemctl --user start podman.socket`
|
||||
- `gh` CLI (used by `mise run vm:setup` to download pre-built runtime artifacts)
|
||||
|
||||
## Releases
|
||||
|
||||
@@ -758,12 +758,14 @@ impl VmDriver {
|
||||
sandbox_id: &str,
|
||||
image_ref: &str,
|
||||
) -> Result<String, Status> {
|
||||
if let Some((docker, image_identity)) = self.resolve_local_docker_image(image_ref).await? {
|
||||
if let Some((engine, image_identity)) =
|
||||
self.resolve_local_container_image(image_ref).await?
|
||||
{
|
||||
return self
|
||||
.ensure_cached_local_image_rootfs_archive(
|
||||
sandbox_id,
|
||||
image_ref,
|
||||
&docker,
|
||||
&engine,
|
||||
&image_identity,
|
||||
)
|
||||
.await;
|
||||
@@ -856,31 +858,30 @@ impl VmDriver {
|
||||
Ok(image_identity)
|
||||
}
|
||||
|
||||
async fn resolve_local_docker_image(
|
||||
async fn resolve_local_container_image(
|
||||
&self,
|
||||
image_ref: &str,
|
||||
) -> Result<Option<(Docker, String)>, Status> {
|
||||
let required_local_image = is_openshell_local_build_image_ref(image_ref);
|
||||
let docker = match Docker::connect_with_local_defaults() {
|
||||
Ok(docker) => docker,
|
||||
Err(err) if required_local_image => {
|
||||
let engine = match connect_local_container_engine().await {
|
||||
Some(engine) => engine,
|
||||
None if required_local_image => {
|
||||
return Err(Status::failed_precondition(format!(
|
||||
"failed to connect to local Docker daemon for locally built sandbox image '{image_ref}': {err}"
|
||||
"no container engine (Docker/Podman) available for locally built sandbox image '{image_ref}'"
|
||||
)));
|
||||
}
|
||||
Err(err) => {
|
||||
None => {
|
||||
warn!(
|
||||
image_ref = %image_ref,
|
||||
error = %err,
|
||||
"vm driver: local Docker daemon unavailable, falling back to registry"
|
||||
"vm driver: no local container engine available, falling back to registry"
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
match docker.inspect_image(image_ref).await {
|
||||
match engine.inspect_image(image_ref).await {
|
||||
Ok(inspect) => {
|
||||
if let Some(message) = local_docker_image_platform_mismatch(
|
||||
if let Some(message) = local_image_platform_mismatch(
|
||||
image_ref,
|
||||
inspect.os.as_deref(),
|
||||
inspect.architecture.as_deref(),
|
||||
@@ -891,30 +892,28 @@ impl VmDriver {
|
||||
warn!(
|
||||
image_ref = %image_ref,
|
||||
%message,
|
||||
"vm driver: local Docker image platform mismatch, falling back to registry"
|
||||
"vm driver: local container image platform mismatch, falling back to registry"
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let image_identity =
|
||||
inspect
|
||||
.id
|
||||
.filter(|id| !id.trim().is_empty())
|
||||
.ok_or_else(|| {
|
||||
Status::failed_precondition(format!(
|
||||
"local Docker image '{image_ref}' inspect response has no image ID"
|
||||
))
|
||||
})?;
|
||||
let image_identity = inspect.id.filter(|id| !id.trim().is_empty()).ok_or_else(
|
||||
|| {
|
||||
Status::failed_precondition(format!(
|
||||
"local container image '{image_ref}' inspect response has no image ID"
|
||||
))
|
||||
},
|
||||
)?;
|
||||
info!(
|
||||
image_ref = %image_ref,
|
||||
image_identity = %image_identity,
|
||||
"vm driver: resolved image from local Docker daemon"
|
||||
"vm driver: resolved image from local container engine"
|
||||
);
|
||||
Ok(Some((docker, image_identity)))
|
||||
Ok(Some((engine, image_identity)))
|
||||
}
|
||||
Err(err) if is_docker_not_found_error(&err) && required_local_image => {
|
||||
Err(Status::failed_precondition(format!(
|
||||
"locally built sandbox image '{image_ref}' is not present in the local Docker daemon"
|
||||
"locally built sandbox image '{image_ref}' is not present in the local container engine"
|
||||
)))
|
||||
}
|
||||
Err(err) if is_docker_not_found_error(&err) => Ok(None),
|
||||
@@ -925,7 +924,7 @@ impl VmDriver {
|
||||
warn!(
|
||||
image_ref = %image_ref,
|
||||
error = %err,
|
||||
"vm driver: local Docker image inspection failed, falling back to registry"
|
||||
"vm driver: local container image inspection failed, falling back to registry"
|
||||
);
|
||||
Ok(None)
|
||||
}
|
||||
@@ -1517,11 +1516,60 @@ fn parse_registry_reference(image_ref: &str) -> Result<Reference, Status> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Try to connect to a local container engine (Docker or Podman).
|
||||
///
|
||||
/// Tries Docker first (`connect_with_local_defaults`, which respects
|
||||
/// `DOCKER_HOST`). If Docker is unavailable, falls back to the Podman
|
||||
/// socket, which exposes a Docker-compatible API.
|
||||
async fn connect_local_container_engine() -> Option<Docker> {
|
||||
if let Ok(docker) = Docker::connect_with_local_defaults() {
|
||||
if docker.ping().await.is_ok() {
|
||||
return Some(docker);
|
||||
}
|
||||
}
|
||||
|
||||
let podman_socket = podman_socket_path();
|
||||
if podman_socket.exists() {
|
||||
if let Ok(docker) =
|
||||
Docker::connect_with_unix(podman_socket.to_str()?, 120, bollard::API_DEFAULT_VERSION)
|
||||
{
|
||||
if docker.ping().await.is_ok() {
|
||||
info!(
|
||||
socket = %podman_socket.display(),
|
||||
"vm driver: connected to Podman (Docker-compatible API)"
|
||||
);
|
||||
return Some(docker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Podman user socket path for the current platform.
|
||||
fn podman_socket_path() -> PathBuf {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let home = std::env::var("HOME").unwrap_or_default();
|
||||
PathBuf::from(home).join(".local/share/containers/podman/machine/podman.sock")
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
std::env::var("XDG_RUNTIME_DIR").map_or_else(
|
||||
|_| {
|
||||
let uid = nix::unistd::getuid();
|
||||
PathBuf::from(format!("/run/user/{uid}/podman/podman.sock"))
|
||||
},
|
||||
|xdg| PathBuf::from(xdg).join("podman/podman.sock"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_openshell_local_build_image_ref(image_ref: &str) -> bool {
|
||||
image_ref.starts_with("openshell/sandbox-from:")
|
||||
}
|
||||
|
||||
fn local_docker_image_platform_mismatch(
|
||||
fn local_image_platform_mismatch(
|
||||
image_ref: &str,
|
||||
actual_os: Option<&str>,
|
||||
actual_arch: Option<&str>,
|
||||
@@ -2918,9 +2966,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_docker_image_platform_mismatch_checks_guest_platform() {
|
||||
fn local_image_platform_mismatch_checks_guest_platform() {
|
||||
assert!(
|
||||
local_docker_image_platform_mismatch(
|
||||
local_image_platform_mismatch(
|
||||
"openshell/sandbox-from:123",
|
||||
Some("linux"),
|
||||
Some(linux_oci_arch()),
|
||||
@@ -2928,7 +2976,7 @@ mod tests {
|
||||
.is_none()
|
||||
);
|
||||
|
||||
let err = local_docker_image_platform_mismatch(
|
||||
let err = local_image_platform_mismatch(
|
||||
"openshell/sandbox-from:123",
|
||||
Some("linux"),
|
||||
Some("wrong-arch"),
|
||||
@@ -2937,7 +2985,7 @@ mod tests {
|
||||
assert!(err.contains("wrong-arch"));
|
||||
assert!(err.contains(linux_oci_arch()));
|
||||
|
||||
let err = local_docker_image_platform_mismatch("openshell/sandbox-from:123", None, None)
|
||||
let err = local_image_platform_mismatch("openshell/sandbox-from:123", None, None)
|
||||
.expect("unknown platform should be reported");
|
||||
assert!(err.contains("unknown/unknown"));
|
||||
}
|
||||
|
||||
@@ -104,6 +104,14 @@ For a service, set `OPENSHELL_DRIVERS=vm` in the service environment file and re
|
||||
|
||||
The gateway starts `openshell-driver-vm` over a private Unix socket and passes its process ID so the driver can reject unexpected local clients. The driver's standalone TCP listener is disabled unless `--allow-unauthenticated-tcp` is set for local development.
|
||||
|
||||
### Local image resolution
|
||||
|
||||
The VM driver resolves sandbox images from a local container engine before falling back to registry pulls. It tries Docker first, then falls back to the Podman socket (Docker-compatible API). On Linux with Podman, enable the API socket so the driver can find local images:
|
||||
|
||||
```shell
|
||||
systemctl --user start podman.socket
|
||||
```
|
||||
|
||||
## Kubernetes Driver
|
||||
|
||||
Kubernetes-backed sandboxes run as pods in the configured sandbox namespace. Use Kubernetes for shared clusters, remote compute, GPU scheduling, and operator-managed environments.
|
||||
|
||||
Reference in New Issue
Block a user