diff --git a/crates/openshell-driver-vm/README.md b/crates/openshell-driver-vm/README.md index 49f2ef005..371c6cefe 100644 --- a/crates/openshell-driver-vm/README.md +++ b/crates/openshell-driver-vm/README.md @@ -166,9 +166,12 @@ The VM guest's serial console is appended to `//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 diff --git a/crates/openshell-driver-vm/src/driver.rs b/crates/openshell-driver-vm/src/driver.rs index 7b9075436..4cea6be1a 100644 --- a/crates/openshell-driver-vm/src/driver.rs +++ b/crates/openshell-driver-vm/src/driver.rs @@ -758,12 +758,14 @@ impl VmDriver { sandbox_id: &str, image_ref: &str, ) -> Result { - 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, 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 { }) } +/// 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 { + 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")); } diff --git a/docs/reference/sandbox-compute-drivers.mdx b/docs/reference/sandbox-compute-drivers.mdx index 4ba8ad385..88d2da3f8 100644 --- a/docs/reference/sandbox-compute-drivers.mdx +++ b/docs/reference/sandbox-compute-drivers.mdx @@ -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.