mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-17 05:31:16 +08:00
* fix(driver-podman): resolve Podman socket via auto-detection Align Podman socket selection with the existing Docker model: explicit config wins, otherwise probe openshell-core for a responsive socket. This also fixes the original HOME-unset panic, since resolution no longer hardcodes a per-OS default path. - add detect_podman_socket() in openshell-core, mirroring detect_docker_socket - PodmanComputeConfig.socket_path is now Option<PathBuf>, no default - remove default_socket_path() (podman driver) and podman_socket_path() (vm driver), both replaced by the shared detector - update server env override and CLI for the new Option type - add tests: responsive-candidate detection in openshell-core, and config-error (not panic) when no socket is configured or reachable * fix(driver-podman): address review feedback on socket resolution Extract socket resolution into resolve_socket_path, taking the detector as a parameter so tests do not depend on real env vars or the host's actual Podman state. Replace the flaky env-mutating test with three deterministic cases: explicit wins, detected is used when absent, and neither source errors. Fix the socket_path doc comment to describe it from a config user's point of view, matching DockerComputeConfig's docstring. Drop a comment that only made sense next to the Docker driver code. Update the Podman README and gateway docs: they described a fixed per-OS default path that no longer exists, replace with the actual probe-then-fail behavior. * docs(driver-podman): simplify socket default description Previous wording was self-contradictory (says auto-detect on unset, then lists the same var as a probed candidate) and omitted the Linux /run/user/uid/podman/podman.sock candidate.
248 lines
8.2 KiB
Rust
248 lines
8.2 KiB
Rust
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
#![allow(clippy::result_large_err)] // gRPC handlers return Result<_, tonic::Status>
|
|
|
|
use futures::{Stream, StreamExt};
|
|
use openshell_core::proto::compute::v1::{
|
|
CreateSandboxRequest, CreateSandboxResponse, DeleteSandboxRequest, DeleteSandboxResponse,
|
|
GetCapabilitiesRequest, GetCapabilitiesResponse, GetSandboxRequest, GetSandboxResponse,
|
|
ListSandboxesRequest, ListSandboxesResponse, StopSandboxRequest, StopSandboxResponse,
|
|
ValidateSandboxCreateRequest, ValidateSandboxCreateResponse, WatchSandboxesEvent,
|
|
WatchSandboxesRequest, compute_driver_server::ComputeDriver,
|
|
};
|
|
use std::pin::Pin;
|
|
use tonic::{Request, Response, Status};
|
|
|
|
use crate::PodmanComputeDriver;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ComputeDriverService {
|
|
driver: PodmanComputeDriver,
|
|
}
|
|
|
|
impl ComputeDriverService {
|
|
#[must_use]
|
|
pub fn new(driver: PodmanComputeDriver) -> Self {
|
|
Self { driver }
|
|
}
|
|
}
|
|
|
|
#[tonic::async_trait]
|
|
impl ComputeDriver for ComputeDriverService {
|
|
async fn get_capabilities(
|
|
&self,
|
|
_request: Request<GetCapabilitiesRequest>,
|
|
) -> Result<Response<GetCapabilitiesResponse>, Status> {
|
|
self.driver
|
|
.capabilities()
|
|
.map(Response::new)
|
|
.map_err(Status::from)
|
|
}
|
|
|
|
async fn validate_sandbox_create(
|
|
&self,
|
|
request: Request<ValidateSandboxCreateRequest>,
|
|
) -> Result<Response<ValidateSandboxCreateResponse>, Status> {
|
|
let sandbox = request
|
|
.into_inner()
|
|
.sandbox
|
|
.ok_or_else(|| Status::invalid_argument("sandbox is required"))?;
|
|
self.driver
|
|
.validate_sandbox_create(&sandbox)
|
|
.await
|
|
.map_err(Status::from)?;
|
|
Ok(Response::new(ValidateSandboxCreateResponse {}))
|
|
}
|
|
|
|
async fn get_sandbox(
|
|
&self,
|
|
request: Request<GetSandboxRequest>,
|
|
) -> Result<Response<GetSandboxResponse>, Status> {
|
|
let request = request.into_inner();
|
|
if request.sandbox_id.is_empty() {
|
|
return Err(Status::invalid_argument("sandbox_id is required"));
|
|
}
|
|
|
|
let sandbox = self
|
|
.driver
|
|
.get_sandbox(&request.sandbox_id)
|
|
.await
|
|
.map_err(Status::from)?
|
|
.ok_or_else(|| Status::not_found("sandbox not found"))?;
|
|
|
|
Ok(Response::new(GetSandboxResponse {
|
|
sandbox: Some(sandbox),
|
|
}))
|
|
}
|
|
|
|
async fn list_sandboxes(
|
|
&self,
|
|
_request: Request<ListSandboxesRequest>,
|
|
) -> Result<Response<ListSandboxesResponse>, Status> {
|
|
let sandboxes = self.driver.list_sandboxes().await.map_err(Status::from)?;
|
|
Ok(Response::new(ListSandboxesResponse { sandboxes }))
|
|
}
|
|
|
|
async fn create_sandbox(
|
|
&self,
|
|
request: Request<CreateSandboxRequest>,
|
|
) -> Result<Response<CreateSandboxResponse>, Status> {
|
|
let sandbox = request
|
|
.into_inner()
|
|
.sandbox
|
|
.ok_or_else(|| Status::invalid_argument("sandbox is required"))?;
|
|
self.driver
|
|
.create_sandbox(&sandbox)
|
|
.await
|
|
.map_err(Status::from)?;
|
|
Ok(Response::new(CreateSandboxResponse {}))
|
|
}
|
|
|
|
async fn stop_sandbox(
|
|
&self,
|
|
request: Request<StopSandboxRequest>,
|
|
) -> Result<Response<StopSandboxResponse>, Status> {
|
|
let request = request.into_inner();
|
|
if request.sandbox_id.is_empty() {
|
|
return Err(Status::invalid_argument("sandbox_id is required"));
|
|
}
|
|
self.driver
|
|
.stop_sandbox(&request.sandbox_id)
|
|
.await
|
|
.map_err(Status::from)?;
|
|
Ok(Response::new(StopSandboxResponse {}))
|
|
}
|
|
|
|
async fn delete_sandbox(
|
|
&self,
|
|
request: Request<DeleteSandboxRequest>,
|
|
) -> Result<Response<DeleteSandboxResponse>, Status> {
|
|
let request = request.into_inner();
|
|
if request.sandbox_id.is_empty() {
|
|
return Err(Status::invalid_argument("sandbox_id is required"));
|
|
}
|
|
let deleted = self
|
|
.driver
|
|
.delete_sandbox(&request.sandbox_id)
|
|
.await
|
|
.map_err(Status::from)?;
|
|
Ok(Response::new(DeleteSandboxResponse { deleted }))
|
|
}
|
|
|
|
type WatchSandboxesStream =
|
|
Pin<Box<dyn Stream<Item = Result<WatchSandboxesEvent, Status>> + Send + 'static>>;
|
|
|
|
async fn watch_sandboxes(
|
|
&self,
|
|
_request: Request<WatchSandboxesRequest>,
|
|
) -> Result<Response<Self::WatchSandboxesStream>, Status> {
|
|
let stream = self.driver.watch_sandboxes().await.map_err(Status::from)?;
|
|
let stream = stream.map(|item| item.map_err(|err| Status::internal(err.to_string())));
|
|
Ok(Response::new(Box::pin(stream)))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::config::PodmanComputeConfig;
|
|
use crate::container;
|
|
use crate::test_utils::{StubResponse, spawn_podman_stub, unique_socket_path};
|
|
use hyper::StatusCode;
|
|
use openshell_core::ComputeDriverError;
|
|
use std::path::PathBuf;
|
|
|
|
#[test]
|
|
fn precondition_driver_errors_map_to_failed_precondition_status() {
|
|
let status: Status =
|
|
ComputeDriverError::Precondition("sandbox container is not running".to_string()).into();
|
|
|
|
assert_eq!(status.code(), tonic::Code::FailedPrecondition);
|
|
assert_eq!(status.message(), "sandbox container is not running");
|
|
}
|
|
|
|
#[test]
|
|
fn already_exists_driver_errors_map_to_already_exists_status() {
|
|
let status: Status = ComputeDriverError::AlreadyExists.into();
|
|
assert_eq!(status.code(), tonic::Code::AlreadyExists);
|
|
}
|
|
|
|
fn test_service(socket_path: PathBuf) -> ComputeDriverService {
|
|
let config = PodmanComputeConfig {
|
|
socket_path: Some(socket_path),
|
|
stop_timeout_secs: 10,
|
|
..PodmanComputeConfig::default()
|
|
};
|
|
ComputeDriverService::new(PodmanComputeDriver::for_tests(config))
|
|
}
|
|
|
|
fn api_path(path: &str) -> String {
|
|
format!("/v5.0.0{path}")
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn delete_sandbox_rejects_missing_sandbox_id() {
|
|
let service = test_service(unique_socket_path("missing-id"));
|
|
|
|
let err = ComputeDriver::delete_sandbox(
|
|
&service,
|
|
Request::new(DeleteSandboxRequest {
|
|
sandbox_id: String::new(),
|
|
sandbox_name: "demo".to_string(),
|
|
}),
|
|
)
|
|
.await
|
|
.expect_err("missing sandbox_id should fail");
|
|
|
|
assert_eq!(err.code(), tonic::Code::InvalidArgument);
|
|
assert_eq!(err.message(), "sandbox_id is required");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn delete_sandbox_forwards_request_sandbox_id_to_driver_cleanup() {
|
|
let sandbox_id = "sandbox-abc";
|
|
let volume_name = container::volume_name(sandbox_id);
|
|
let (socket_path, request_log, handle) = spawn_podman_stub(
|
|
"forward-id",
|
|
vec![
|
|
// list_containers returns empty (container already gone)
|
|
StubResponse::new(StatusCode::OK, "[]"),
|
|
// remove_volume
|
|
StubResponse::new(StatusCode::NO_CONTENT, ""),
|
|
],
|
|
);
|
|
let service = test_service(socket_path.clone());
|
|
|
|
let response = ComputeDriver::delete_sandbox(
|
|
&service,
|
|
Request::new(DeleteSandboxRequest {
|
|
sandbox_id: sandbox_id.to_string(),
|
|
sandbox_name: "demo".to_string(),
|
|
}),
|
|
)
|
|
.await
|
|
.expect("delete should succeed")
|
|
.into_inner();
|
|
|
|
assert!(
|
|
!response.deleted,
|
|
"already-removed containers should still report deleted=false"
|
|
);
|
|
handle.await.expect("stub task should finish");
|
|
let requests = request_log
|
|
.lock()
|
|
.expect("request log lock should not be poisoned")
|
|
.clone();
|
|
assert!(requests[0].contains("/libpod/containers/json"));
|
|
assert_eq!(
|
|
requests[1],
|
|
format!(
|
|
"DELETE {}",
|
|
api_path(&format!("/libpod/volumes/{volume_name}"))
|
|
)
|
|
);
|
|
let _ = std::fs::remove_file(socket_path);
|
|
}
|
|
}
|