mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-30 07:33:59 +08:00
feat(windows): stub unsupported compute drivers
Signed-off-by: Shailendra Singh <shailendras@nvidia.com> Signed-off-by: Akber Raza <akberr@nvidia.com>
This commit is contained in:
committed by
Akber Raza
parent
34a0323b2e
commit
b17118ef14
@@ -12,14 +12,15 @@ repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
openshell-core = { path = "../openshell-core", default-features = false }
|
||||
serde = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
tokio = { workspace = true }
|
||||
tonic = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
prost-types = { workspace = true }
|
||||
bollard = { version = "0.20" }
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Docker compute driver.
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
|
||||
#![allow(clippy::result_large_err)]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows::{DockerComputeConfig, default_docker_supervisor_image};
|
||||
|
||||
use bollard::Docker;
|
||||
use bollard::errors::Error as BollardError;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use openshell_core::config::DEFAULT_DOCKER_NETWORK_NAME;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const DEFAULT_DOCKER_SUPERVISOR_IMAGE_REPO: &str = "ghcr.io/nvidia/openshell/supervisor";
|
||||
const DEFAULT_SANDBOX_PIDS_LIMIT: i64 = 2048;
|
||||
|
||||
#[must_use]
|
||||
pub fn default_docker_supervisor_image() -> String {
|
||||
format!(
|
||||
"{DEFAULT_DOCKER_SUPERVISOR_IMAGE_REPO}:{}",
|
||||
default_docker_supervisor_image_tag()
|
||||
)
|
||||
}
|
||||
|
||||
fn default_docker_supervisor_image_tag() -> String {
|
||||
let tag = option_env!("OPENSHELL_IMAGE_TAG")
|
||||
.filter(|tag| !tag.is_empty())
|
||||
.or_else(|| option_env!("IMAGE_TAG").filter(|tag| !tag.is_empty()))
|
||||
.unwrap_or_else(|| {
|
||||
if env!("CARGO_PKG_VERSION").is_empty() || env!("CARGO_PKG_VERSION") == "0.0.0" {
|
||||
"dev"
|
||||
} else {
|
||||
env!("CARGO_PKG_VERSION")
|
||||
}
|
||||
});
|
||||
|
||||
tag.replace('+', "-")
|
||||
}
|
||||
|
||||
/// Gateway-local configuration for the Docker compute driver.
|
||||
///
|
||||
/// Windows builds keep this type so existing config files continue to parse,
|
||||
/// but Docker runtime support is intentionally unavailable on Windows.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct DockerComputeConfig {
|
||||
pub socket_path: Option<PathBuf>,
|
||||
pub default_image: String,
|
||||
pub image_pull_policy: String,
|
||||
pub sandbox_namespace: String,
|
||||
pub grpc_endpoint: String,
|
||||
pub supervisor_bin: Option<PathBuf>,
|
||||
pub supervisor_image: Option<String>,
|
||||
pub guest_tls_ca: Option<PathBuf>,
|
||||
pub guest_tls_cert: Option<PathBuf>,
|
||||
pub guest_tls_key: Option<PathBuf>,
|
||||
pub network_name: String,
|
||||
pub host_gateway_ip: String,
|
||||
pub ssh_socket_path: String,
|
||||
pub sandbox_pids_limit: i64,
|
||||
pub enable_bind_mounts: bool,
|
||||
}
|
||||
|
||||
impl Default for DockerComputeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
socket_path: None,
|
||||
default_image: openshell_core::image::default_sandbox_image(),
|
||||
image_pull_policy: String::new(),
|
||||
sandbox_namespace: "default".to_string(),
|
||||
grpc_endpoint: String::new(),
|
||||
supervisor_bin: None,
|
||||
supervisor_image: None,
|
||||
guest_tls_ca: None,
|
||||
guest_tls_cert: None,
|
||||
guest_tls_key: None,
|
||||
network_name: DEFAULT_DOCKER_NETWORK_NAME.to_string(),
|
||||
host_gateway_ip: String::new(),
|
||||
ssh_socket_path: "/run/openshell/ssh.sock".to_string(),
|
||||
sandbox_pids_limit: DEFAULT_SANDBOX_PIDS_LIMIT,
|
||||
enable_bind_mounts: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,9 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
openshell-core = { path = "../openshell-core", default-features = false }
|
||||
openshell-policy = { path = "../openshell-policy" }
|
||||
serde = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
tokio = { workspace = true }
|
||||
tonic = { workspace = true, features = ["transport"] }
|
||||
prost = { workspace = true }
|
||||
@@ -27,7 +29,6 @@ tokio-stream = { workspace = true }
|
||||
kube = { workspace = true }
|
||||
kube-runtime = { workspace = true }
|
||||
k8s-openapi = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
@@ -37,6 +38,7 @@ miette = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
temp-env = "0.3"
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod config;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub mod driver;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub mod grpc;
|
||||
|
||||
pub use config::{
|
||||
@@ -10,5 +13,8 @@ pub use config::{
|
||||
DEFAULT_WORKSPACE_STORAGE_SIZE, KubernetesComputeConfig, KubernetesSidecarConfig,
|
||||
SupervisorSideloadMethod, SupervisorTopology,
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use driver::{KubernetesComputeDriver, KubernetesDriverError};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use grpc::ComputeDriverService;
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::{ArgAction, Parser};
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use std::net::SocketAddr;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
include!("main_unix.rs");
|
||||
|
||||
use openshell_core::VERSION;
|
||||
use openshell_core::proto::compute::v1::compute_driver_server::ComputeDriverServer;
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::{ArgAction, Parser};
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use std::net::SocketAddr;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use openshell_core::VERSION;
|
||||
use openshell_core::proto::compute::v1::compute_driver_server::ComputeDriverServer;
|
||||
use openshell_driver_kubernetes::{
|
||||
AppArmorProfile, ComputeDriverService, DEFAULT_PROXY_UID, DEFAULT_SANDBOX_SERVICE_ACCOUNT_NAME,
|
||||
KubernetesComputeConfig, KubernetesComputeDriver, KubernetesSidecarConfig,
|
||||
SupervisorSideloadMethod, SupervisorTopology,
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "openshell-driver-kubernetes")]
|
||||
#[command(version = VERSION)]
|
||||
struct Args {
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_COMPUTE_DRIVER_BIND",
|
||||
default_value = "127.0.0.1:50061"
|
||||
)]
|
||||
bind_address: SocketAddr,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_LOG_LEVEL", default_value = "info")]
|
||||
log_level: String,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_NAMESPACE", default_value = "default")]
|
||||
sandbox_namespace: String,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_K8S_SANDBOX_SERVICE_ACCOUNT",
|
||||
default_value = DEFAULT_SANDBOX_SERVICE_ACCOUNT_NAME
|
||||
)]
|
||||
sandbox_service_account: String,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_IMAGE")]
|
||||
sandbox_image: Option<String>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_IMAGE_PULL_POLICY")]
|
||||
sandbox_image_pull_policy: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_SANDBOX_IMAGE_PULL_SECRETS",
|
||||
value_delimiter = ','
|
||||
)]
|
||||
sandbox_image_pull_secrets: Vec<String>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_GRPC_ENDPOINT")]
|
||||
grpc_endpoint: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_SANDBOX_SSH_SOCKET_PATH",
|
||||
default_value = "/run/openshell/ssh.sock"
|
||||
)]
|
||||
sandbox_ssh_socket_path: String,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_CLIENT_TLS_SECRET_NAME")]
|
||||
client_tls_secret_name: Option<String>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_HOST_GATEWAY_IP")]
|
||||
host_gateway_ip: Option<String>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE")]
|
||||
supervisor_image: Option<String>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE_PULL_POLICY")]
|
||||
supervisor_image_pull_policy: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_SUPERVISOR_SIDELOAD_METHOD",
|
||||
default_value = "image-volume"
|
||||
)]
|
||||
supervisor_sideload_method: SupervisorSideloadMethod,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_K8S_TOPOLOGY", default_value = "combined")]
|
||||
topology: SupervisorTopology,
|
||||
|
||||
#[arg(
|
||||
long = "sidecar-proxy-uid",
|
||||
alias = "proxy-uid",
|
||||
env = "OPENSHELL_K8S_SIDECAR_PROXY_UID",
|
||||
default_value_t = DEFAULT_PROXY_UID
|
||||
)]
|
||||
sidecar_proxy_uid: u32,
|
||||
|
||||
#[arg(
|
||||
long = "sidecar-process-binary-aware-network-policy",
|
||||
env = "OPENSHELL_K8S_SIDECAR_PROCESS_BINARY_AWARE_NETWORK_POLICY",
|
||||
default_value_t = true,
|
||||
action = ArgAction::Set
|
||||
)]
|
||||
sidecar_process_binary_aware_network_policy: bool,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_ENABLE_USER_NAMESPACES")]
|
||||
enable_user_namespaces: bool,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_K8S_APP_ARMOR_PROFILE")]
|
||||
app_armor_profile: Option<AppArmorProfile>,
|
||||
|
||||
/// Lifetime (seconds) of the projected `ServiceAccount` token
|
||||
/// kubelet writes into each sandbox pod for the `IssueSandboxToken`
|
||||
/// bootstrap exchange. Kubelet enforces a minimum of 600s; the
|
||||
/// gateway clamps values outside `[600, 86400]`. Default 3600.
|
||||
#[arg(long, env = "OPENSHELL_K8S_SA_TOKEN_TTL_SECS", default_value_t = 3600)]
|
||||
sa_token_ttl_secs: i64,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_PROVIDER_SPIFFE_WORKLOAD_API_SOCKET")]
|
||||
provider_spiffe_workload_api_socket_path: Option<String>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_K8S_SANDBOX_UID")]
|
||||
sandbox_uid: Option<u32>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_K8S_SANDBOX_GID")]
|
||||
sandbox_gid: Option<u32>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)),
|
||||
)
|
||||
.init();
|
||||
|
||||
let driver = KubernetesComputeDriver::new(KubernetesComputeConfig {
|
||||
namespace: args.sandbox_namespace,
|
||||
service_account_name: args.sandbox_service_account,
|
||||
default_image: args.sandbox_image.unwrap_or_default(),
|
||||
image_pull_policy: args.sandbox_image_pull_policy.unwrap_or_default(),
|
||||
image_pull_secrets: args.sandbox_image_pull_secrets,
|
||||
supervisor_image: args
|
||||
.supervisor_image
|
||||
.unwrap_or_else(openshell_core::config::default_supervisor_image),
|
||||
supervisor_image_pull_policy: args.supervisor_image_pull_policy.unwrap_or_default(),
|
||||
supervisor_sideload_method: args.supervisor_sideload_method,
|
||||
topology: args.topology,
|
||||
sidecar: KubernetesSidecarConfig {
|
||||
proxy_uid: args.sidecar_proxy_uid,
|
||||
process_binary_aware_network_policy: args.sidecar_process_binary_aware_network_policy,
|
||||
},
|
||||
grpc_endpoint: args.grpc_endpoint.unwrap_or_default(),
|
||||
ssh_socket_path: args.sandbox_ssh_socket_path,
|
||||
client_tls_secret_name: args.client_tls_secret_name.unwrap_or_default(),
|
||||
host_gateway_ip: args.host_gateway_ip.unwrap_or_default(),
|
||||
enable_user_namespaces: args.enable_user_namespaces,
|
||||
app_armor_profile: args.app_armor_profile,
|
||||
workspace_default_storage_size: std::env::var(
|
||||
"OPENSHELL_K8S_WORKSPACE_DEFAULT_STORAGE_SIZE",
|
||||
)
|
||||
.unwrap_or_else(|_| {
|
||||
openshell_driver_kubernetes::DEFAULT_WORKSPACE_STORAGE_SIZE.to_string()
|
||||
}),
|
||||
default_runtime_class_name: std::env::var("OPENSHELL_K8S_DEFAULT_RUNTIME_CLASS_NAME")
|
||||
.unwrap_or_default(),
|
||||
sa_token_ttl_secs: args.sa_token_ttl_secs,
|
||||
provider_spiffe_workload_api_socket_path: args
|
||||
.provider_spiffe_workload_api_socket_path
|
||||
.unwrap_or_default(),
|
||||
sandbox_uid: args.sandbox_uid,
|
||||
sandbox_gid: args.sandbox_gid,
|
||||
})
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
|
||||
info!(address = %args.bind_address, "Starting Kubernetes compute driver");
|
||||
tonic::transport::Server::builder()
|
||||
.add_service(ComputeDriverServer::new(ComputeDriverService::new(driver)))
|
||||
.serve(args.bind_address)
|
||||
.await
|
||||
.into_diagnostic()
|
||||
}
|
||||
@@ -14,7 +14,10 @@ repository.workspace = true
|
||||
name = "openshell-driver-podman"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
serde = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
openshell-core = { path = "../openshell-core", default-features = false }
|
||||
|
||||
tokio = { workspace = true }
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub(crate) mod client;
|
||||
pub mod config;
|
||||
pub(crate) mod container;
|
||||
pub mod driver;
|
||||
pub mod grpc;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_utils;
|
||||
pub(crate) mod watcher;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
|
||||
pub use config::PodmanComputeConfig;
|
||||
pub use driver::PodmanComputeDriver;
|
||||
pub use grpc::ComputeDriverService;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows::PodmanComputeConfig;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
include!("lib_unix.rs");
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub(crate) mod client;
|
||||
pub mod config;
|
||||
pub(crate) mod container;
|
||||
pub mod driver;
|
||||
pub mod grpc;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_utils;
|
||||
pub(crate) mod watcher;
|
||||
|
||||
pub use config::PodmanComputeConfig;
|
||||
pub use driver::PodmanComputeDriver;
|
||||
pub use grpc::ComputeDriverService;
|
||||
@@ -1,185 +1,11 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::Parser;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use openshell_core::VERSION;
|
||||
use openshell_core::proto::compute::v1::compute_driver_server::ComputeDriverServer;
|
||||
use openshell_driver_podman::config::{
|
||||
DEFAULT_NETWORK_NAME, DEFAULT_PODMAN_STOP_TIMEOUT_SECS, DEFAULT_SANDBOX_PIDS_LIMIT,
|
||||
ImagePullPolicy,
|
||||
};
|
||||
use openshell_driver_podman::{ComputeDriverService, PodmanComputeConfig, PodmanComputeDriver};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "openshell-driver-podman")]
|
||||
#[command(version = VERSION)]
|
||||
struct Args {
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_COMPUTE_DRIVER_BIND",
|
||||
default_value = "127.0.0.1:50061"
|
||||
)]
|
||||
bind_address: SocketAddr,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_LOG_LEVEL", default_value = "info")]
|
||||
log_level: String,
|
||||
|
||||
/// Path to the Podman API Unix socket.
|
||||
#[arg(long, env = "OPENSHELL_PODMAN_SOCKET")]
|
||||
podman_socket: Option<PathBuf>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_IMAGE")]
|
||||
sandbox_image: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_SANDBOX_IMAGE_PULL_POLICY",
|
||||
default_value_t = ImagePullPolicy::Missing
|
||||
)]
|
||||
sandbox_image_pull_policy: ImagePullPolicy,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_GRPC_ENDPOINT")]
|
||||
grpc_endpoint: Option<String>,
|
||||
|
||||
/// Port the gateway server is listening on.
|
||||
///
|
||||
/// Used when `--grpc-endpoint` is not set to auto-detect the endpoint
|
||||
/// that sandbox containers dial back to.
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_GATEWAY_PORT",
|
||||
default_value_t = openshell_core::config::DEFAULT_SERVER_PORT
|
||||
)]
|
||||
gateway_port: u16,
|
||||
|
||||
/// Host gateway IP used for sandbox host aliases.
|
||||
///
|
||||
/// Empty uses Podman's `host-gateway` resolver.
|
||||
#[arg(long, env = "OPENSHELL_PODMAN_HOST_GATEWAY_IP")]
|
||||
host_gateway_ip: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_SANDBOX_SSH_SOCKET_PATH",
|
||||
default_value = openshell_core::container_paths::SSH_SOCKET_PATH
|
||||
)]
|
||||
sandbox_ssh_socket_path: String,
|
||||
|
||||
/// Podman bridge network name.
|
||||
#[arg(long, env = "OPENSHELL_NETWORK_NAME", default_value = DEFAULT_NETWORK_NAME)]
|
||||
network_name: String,
|
||||
|
||||
/// Container stop timeout in seconds (SIGTERM → SIGKILL).
|
||||
#[arg(long, env = "OPENSHELL_STOP_TIMEOUT", default_value_t = DEFAULT_PODMAN_STOP_TIMEOUT_SECS)]
|
||||
stop_timeout: u32,
|
||||
|
||||
/// Container cgroup PID limit for sandbox containers. Set 0 to inherit
|
||||
/// Podman's runtime/default PID limit.
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_SANDBOX_PIDS_LIMIT",
|
||||
default_value_t = DEFAULT_SANDBOX_PIDS_LIMIT
|
||||
)]
|
||||
sandbox_pids_limit: i64,
|
||||
|
||||
/// OCI image containing the openshell-sandbox supervisor binary.
|
||||
#[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE")]
|
||||
supervisor_image: Option<String>,
|
||||
|
||||
/// Host path to the CA certificate for sandbox mTLS.
|
||||
#[arg(long, env = "OPENSHELL_PODMAN_TLS_CA")]
|
||||
podman_tls_ca: Option<PathBuf>,
|
||||
|
||||
/// Host path to the client certificate for sandbox mTLS.
|
||||
#[arg(long, env = "OPENSHELL_PODMAN_TLS_CERT")]
|
||||
podman_tls_cert: Option<PathBuf>,
|
||||
|
||||
/// Host path to the client private key for sandbox mTLS.
|
||||
#[arg(long, env = "OPENSHELL_PODMAN_TLS_KEY")]
|
||||
podman_tls_key: Option<PathBuf>,
|
||||
|
||||
/// Corporate forward proxy URL for the supervisor's upstream TLS dials,
|
||||
/// in explicit `http://host:port` form (scheme and port required).
|
||||
/// Credentials must not be embedded in the URL; use
|
||||
/// `--sandbox-proxy-auth-file` instead.
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_HTTPS_PROXY")]
|
||||
sandbox_https_proxy: Option<String>,
|
||||
|
||||
/// Comma-separated `NO_PROXY` list injected alongside the proxy URL.
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_NO_PROXY")]
|
||||
sandbox_no_proxy: Option<String>,
|
||||
|
||||
/// Path to a file containing the corporate proxy credentials as
|
||||
/// `user:pass`. Delivered to the supervisor through a root-only secret
|
||||
/// mount so the credentials never appear in config or container metadata.
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_PROXY_AUTH_FILE")]
|
||||
sandbox_proxy_auth_file: Option<String>,
|
||||
|
||||
/// Explicit acknowledgement (`true`) that the proxy credential is sent
|
||||
/// as cleartext Basic auth over the plain-TCP connection to the http://
|
||||
/// proxy. Required when `--sandbox-proxy-auth-file` is set.
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_PROXY_AUTH_ALLOW_INSECURE")]
|
||||
sandbox_proxy_auth_allow_insecure: Option<bool>,
|
||||
|
||||
/// Send the destination hostname in CONNECT requests to the corporate
|
||||
/// proxy instead of a validated IP. Only for proxies whose ACLs filter
|
||||
/// on hostnames: the proxy then resolves the name itself, so sandbox
|
||||
/// SSRF/`allowed_ips` validation no longer binds the connection.
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_PROXY_CONNECT_BY_HOSTNAME")]
|
||||
sandbox_proxy_connect_by_hostname: Option<bool>,
|
||||
#[cfg(target_os = "windows")]
|
||||
fn main() {
|
||||
eprintln!("openshell-driver-podman is unsupported on Windows");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)),
|
||||
)
|
||||
.init();
|
||||
|
||||
let driver = PodmanComputeDriver::new(PodmanComputeConfig {
|
||||
socket_path: args.podman_socket,
|
||||
default_image: args.sandbox_image.unwrap_or_default(),
|
||||
image_pull_policy: args.sandbox_image_pull_policy,
|
||||
grpc_endpoint: args.grpc_endpoint.unwrap_or_default(),
|
||||
gateway_port: args.gateway_port,
|
||||
host_gateway_ip: args
|
||||
.host_gateway_ip
|
||||
.unwrap_or_else(PodmanComputeConfig::default_host_gateway_ip),
|
||||
sandbox_ssh_socket_path: args.sandbox_ssh_socket_path,
|
||||
network_name: args.network_name,
|
||||
stop_timeout_secs: args.stop_timeout,
|
||||
supervisor_image: args
|
||||
.supervisor_image
|
||||
.unwrap_or_else(openshell_core::config::default_supervisor_image),
|
||||
guest_tls_ca: args.podman_tls_ca,
|
||||
guest_tls_cert: args.podman_tls_cert,
|
||||
guest_tls_key: args.podman_tls_key,
|
||||
sandbox_pids_limit: args.sandbox_pids_limit,
|
||||
https_proxy: args.sandbox_https_proxy,
|
||||
no_proxy: args.sandbox_no_proxy,
|
||||
proxy_auth_file: args.sandbox_proxy_auth_file,
|
||||
proxy_auth_allow_insecure: args.sandbox_proxy_auth_allow_insecure,
|
||||
proxy_connect_by_hostname: args.sandbox_proxy_connect_by_hostname,
|
||||
..PodmanComputeConfig::default()
|
||||
})
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
|
||||
info!(address = %args.bind_address, "Starting Podman compute driver");
|
||||
tonic::transport::Server::builder()
|
||||
.add_service(ComputeDriverServer::new(ComputeDriverService::new(driver)))
|
||||
.serve_with_shutdown(args.bind_address, async {
|
||||
tokio::signal::ctrl_c().await.ok();
|
||||
info!("Received shutdown signal, draining in-flight requests");
|
||||
})
|
||||
.await
|
||||
.into_diagnostic()
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
include!("main_unix.rs");
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::Parser;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use openshell_core::VERSION;
|
||||
use openshell_core::proto::compute::v1::compute_driver_server::ComputeDriverServer;
|
||||
use openshell_driver_podman::config::{
|
||||
DEFAULT_NETWORK_NAME, DEFAULT_PODMAN_STOP_TIMEOUT_SECS, DEFAULT_SANDBOX_PIDS_LIMIT,
|
||||
ImagePullPolicy,
|
||||
};
|
||||
use openshell_driver_podman::{ComputeDriverService, PodmanComputeConfig, PodmanComputeDriver};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "openshell-driver-podman")]
|
||||
#[command(version = VERSION)]
|
||||
struct Args {
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_COMPUTE_DRIVER_BIND",
|
||||
default_value = "127.0.0.1:50061"
|
||||
)]
|
||||
bind_address: SocketAddr,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_LOG_LEVEL", default_value = "info")]
|
||||
log_level: String,
|
||||
|
||||
/// Path to the Podman API Unix socket.
|
||||
#[arg(long, env = "OPENSHELL_PODMAN_SOCKET")]
|
||||
podman_socket: Option<PathBuf>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_IMAGE")]
|
||||
sandbox_image: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_SANDBOX_IMAGE_PULL_POLICY",
|
||||
default_value_t = ImagePullPolicy::Missing
|
||||
)]
|
||||
sandbox_image_pull_policy: ImagePullPolicy,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_GRPC_ENDPOINT")]
|
||||
grpc_endpoint: Option<String>,
|
||||
|
||||
/// Port the gateway server is listening on.
|
||||
///
|
||||
/// Used when `--grpc-endpoint` is not set to auto-detect the endpoint
|
||||
/// that sandbox containers dial back to.
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_GATEWAY_PORT",
|
||||
default_value_t = openshell_core::config::DEFAULT_SERVER_PORT
|
||||
)]
|
||||
gateway_port: u16,
|
||||
|
||||
/// Host gateway IP used for sandbox host aliases.
|
||||
///
|
||||
/// Empty uses Podman's `host-gateway` resolver.
|
||||
#[arg(long, env = "OPENSHELL_PODMAN_HOST_GATEWAY_IP")]
|
||||
host_gateway_ip: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_SANDBOX_SSH_SOCKET_PATH",
|
||||
default_value = openshell_core::container_paths::SSH_SOCKET_PATH
|
||||
)]
|
||||
sandbox_ssh_socket_path: String,
|
||||
|
||||
/// Podman bridge network name.
|
||||
#[arg(long, env = "OPENSHELL_NETWORK_NAME", default_value = DEFAULT_NETWORK_NAME)]
|
||||
network_name: String,
|
||||
|
||||
/// Container stop timeout in seconds (SIGTERM → SIGKILL).
|
||||
#[arg(long, env = "OPENSHELL_STOP_TIMEOUT", default_value_t = DEFAULT_PODMAN_STOP_TIMEOUT_SECS)]
|
||||
stop_timeout: u32,
|
||||
|
||||
/// Container cgroup PID limit for sandbox containers. Set 0 to inherit
|
||||
/// Podman's runtime/default PID limit.
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_SANDBOX_PIDS_LIMIT",
|
||||
default_value_t = DEFAULT_SANDBOX_PIDS_LIMIT
|
||||
)]
|
||||
sandbox_pids_limit: i64,
|
||||
|
||||
/// OCI image containing the openshell-sandbox supervisor binary.
|
||||
#[arg(long, env = "OPENSHELL_SUPERVISOR_IMAGE")]
|
||||
supervisor_image: Option<String>,
|
||||
|
||||
/// Host path to the CA certificate for sandbox mTLS.
|
||||
#[arg(long, env = "OPENSHELL_PODMAN_TLS_CA")]
|
||||
podman_tls_ca: Option<PathBuf>,
|
||||
|
||||
/// Host path to the client certificate for sandbox mTLS.
|
||||
#[arg(long, env = "OPENSHELL_PODMAN_TLS_CERT")]
|
||||
podman_tls_cert: Option<PathBuf>,
|
||||
|
||||
/// Host path to the client private key for sandbox mTLS.
|
||||
#[arg(long, env = "OPENSHELL_PODMAN_TLS_KEY")]
|
||||
podman_tls_key: Option<PathBuf>,
|
||||
|
||||
/// Corporate forward proxy URL for the supervisor's upstream TLS dials,
|
||||
/// in explicit `http://host:port` form (scheme and port required).
|
||||
/// Credentials must not be embedded in the URL; use
|
||||
/// `--sandbox-proxy-auth-file` instead.
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_HTTPS_PROXY")]
|
||||
sandbox_https_proxy: Option<String>,
|
||||
|
||||
/// Comma-separated `NO_PROXY` list injected alongside the proxy URL.
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_NO_PROXY")]
|
||||
sandbox_no_proxy: Option<String>,
|
||||
|
||||
/// Path to a file containing the corporate proxy credentials as
|
||||
/// `user:pass`. Delivered to the supervisor through a root-only secret
|
||||
/// mount so the credentials never appear in config or container metadata.
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_PROXY_AUTH_FILE")]
|
||||
sandbox_proxy_auth_file: Option<String>,
|
||||
|
||||
/// Explicit acknowledgement (`true`) that the proxy credential is sent
|
||||
/// as cleartext Basic auth over the plain-TCP connection to the http://
|
||||
/// proxy. Required when `--sandbox-proxy-auth-file` is set.
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_PROXY_AUTH_ALLOW_INSECURE")]
|
||||
sandbox_proxy_auth_allow_insecure: Option<bool>,
|
||||
|
||||
/// Send the destination hostname in CONNECT requests to the corporate
|
||||
/// proxy instead of a validated IP. Only for proxies whose ACLs filter
|
||||
/// on hostnames: the proxy then resolves the name itself, so sandbox
|
||||
/// SSRF/`allowed_ips` validation no longer binds the connection.
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_PROXY_CONNECT_BY_HOSTNAME")]
|
||||
sandbox_proxy_connect_by_hostname: Option<bool>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)),
|
||||
)
|
||||
.init();
|
||||
|
||||
let driver = PodmanComputeDriver::new(PodmanComputeConfig {
|
||||
socket_path: args.podman_socket,
|
||||
default_image: args.sandbox_image.unwrap_or_default(),
|
||||
image_pull_policy: args.sandbox_image_pull_policy,
|
||||
grpc_endpoint: args.grpc_endpoint.unwrap_or_default(),
|
||||
gateway_port: args.gateway_port,
|
||||
host_gateway_ip: args
|
||||
.host_gateway_ip
|
||||
.unwrap_or_else(PodmanComputeConfig::default_host_gateway_ip),
|
||||
sandbox_ssh_socket_path: args.sandbox_ssh_socket_path,
|
||||
network_name: args.network_name,
|
||||
stop_timeout_secs: args.stop_timeout,
|
||||
supervisor_image: args
|
||||
.supervisor_image
|
||||
.unwrap_or_else(openshell_core::config::default_supervisor_image),
|
||||
guest_tls_ca: args.podman_tls_ca,
|
||||
guest_tls_cert: args.podman_tls_cert,
|
||||
guest_tls_key: args.podman_tls_key,
|
||||
sandbox_pids_limit: args.sandbox_pids_limit,
|
||||
https_proxy: args.sandbox_https_proxy,
|
||||
no_proxy: args.sandbox_no_proxy,
|
||||
proxy_auth_file: args.sandbox_proxy_auth_file,
|
||||
proxy_auth_allow_insecure: args.sandbox_proxy_auth_allow_insecure,
|
||||
proxy_connect_by_hostname: args.sandbox_proxy_connect_by_hostname,
|
||||
..PodmanComputeConfig::default()
|
||||
})
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
|
||||
info!(address = %args.bind_address, "Starting Podman compute driver");
|
||||
tonic::transport::Server::builder()
|
||||
.add_service(ComputeDriverServer::new(ComputeDriverService::new(driver)))
|
||||
.serve_with_shutdown(args.bind_address, async {
|
||||
tokio::signal::ctrl_c().await.ok();
|
||||
info!("Received shutdown signal, draining in-flight requests");
|
||||
})
|
||||
.await
|
||||
.into_diagnostic()
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
const DEFAULT_SANDBOX_IMAGE: &str = "ghcr.io/nvidia/openshell/sandbox:latest";
|
||||
const DEFAULT_SUPERVISOR_IMAGE: &str = "ghcr.io/nvidia/openshell/supervisor:latest";
|
||||
const DEFAULT_SERVER_PORT: u16 = 8080;
|
||||
const DEFAULT_STOP_TIMEOUT_SECS: u32 = 10;
|
||||
const DEFAULT_NETWORK_NAME: &str = "openshell";
|
||||
const DEFAULT_SANDBOX_PIDS_LIMIT: i64 = 2048;
|
||||
const DEFAULT_HEALTH_CHECK_INTERVAL_SECS: u64 = 10;
|
||||
|
||||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ImagePullPolicy {
|
||||
Always,
|
||||
#[default]
|
||||
Missing,
|
||||
Never,
|
||||
Newer,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct PodmanComputeConfig {
|
||||
pub socket_path: Option<PathBuf>,
|
||||
pub default_image: String,
|
||||
pub image_pull_policy: ImagePullPolicy,
|
||||
pub grpc_endpoint: String,
|
||||
pub gateway_port: u16,
|
||||
pub host_gateway_ip: String,
|
||||
pub sandbox_ssh_socket_path: String,
|
||||
pub network_name: String,
|
||||
pub stop_timeout_secs: u32,
|
||||
pub supervisor_image: String,
|
||||
pub guest_tls_ca: Option<PathBuf>,
|
||||
pub guest_tls_cert: Option<PathBuf>,
|
||||
pub guest_tls_key: Option<PathBuf>,
|
||||
pub sandbox_pids_limit: i64,
|
||||
pub enable_bind_mounts: bool,
|
||||
pub health_check_interval_secs: u64,
|
||||
}
|
||||
|
||||
impl Default for PodmanComputeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
socket_path: None,
|
||||
default_image: DEFAULT_SANDBOX_IMAGE.to_string(),
|
||||
image_pull_policy: ImagePullPolicy::default(),
|
||||
grpc_endpoint: String::new(),
|
||||
gateway_port: DEFAULT_SERVER_PORT,
|
||||
host_gateway_ip: String::new(),
|
||||
sandbox_ssh_socket_path: String::new(),
|
||||
network_name: DEFAULT_NETWORK_NAME.to_string(),
|
||||
stop_timeout_secs: DEFAULT_STOP_TIMEOUT_SECS,
|
||||
supervisor_image: DEFAULT_SUPERVISOR_IMAGE.to_string(),
|
||||
guest_tls_ca: None,
|
||||
guest_tls_cert: None,
|
||||
guest_tls_key: None,
|
||||
sandbox_pids_limit: DEFAULT_SANDBOX_PIDS_LIMIT,
|
||||
enable_bind_mounts: false,
|
||||
health_check_interval_secs: DEFAULT_HEALTH_CHECK_INTERVAL_SECS,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ path = "src/lib.rs"
|
||||
name = "openshell-driver-vm"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
openshell-core = { path = "../openshell-core", default-features = false }
|
||||
openshell-otel = { path = "../openshell-otel" }
|
||||
openshell-policy = { path = "../openshell-policy" }
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod driver;
|
||||
mod embedded_runtime;
|
||||
mod ffi;
|
||||
pub mod gpu;
|
||||
pub mod lifecycle;
|
||||
mod nft_ruleset;
|
||||
pub mod otel_tracing;
|
||||
pub mod procguard;
|
||||
mod rootfs;
|
||||
mod runtime;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn windows_unsupported() -> &'static str {
|
||||
"openshell-driver-vm is unsupported on Windows"
|
||||
}
|
||||
|
||||
pub use driver::{VmDriver, VmDriverConfig};
|
||||
pub use lifecycle::{
|
||||
BackendFeature, ExtensionCapabilities, ExtensionDescriptor, GuestInitDropin, LaunchAbortReason,
|
||||
LaunchPlan, LifecycleError, LifecycleExtension, LifecycleExtensionRegistry, LifecycleResult,
|
||||
RestoreContext,
|
||||
};
|
||||
pub use runtime::{
|
||||
VM_RUNTIME_DIR_ENV, VmBackend, VmLaunchConfig, cleanup_stale_tap_interfaces,
|
||||
configured_runtime_dir, run_vm,
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
include!("lib_unix.rs");
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod driver;
|
||||
mod embedded_runtime;
|
||||
mod ffi;
|
||||
pub mod gpu;
|
||||
pub mod lifecycle;
|
||||
mod nft_ruleset;
|
||||
pub mod otel_tracing;
|
||||
pub mod procguard;
|
||||
mod rootfs;
|
||||
mod runtime;
|
||||
|
||||
pub use driver::{VmDriver, VmDriverConfig};
|
||||
pub use lifecycle::{
|
||||
BackendFeature, ExtensionCapabilities, ExtensionDescriptor, GuestInitDropin, LaunchAbortReason,
|
||||
LaunchPlan, LifecycleError, LifecycleExtension, LifecycleExtensionRegistry, LifecycleResult,
|
||||
RestoreContext,
|
||||
};
|
||||
pub use runtime::{
|
||||
VM_RUNTIME_DIR_ENV, VmBackend, VmLaunchConfig, cleanup_stale_tap_interfaces,
|
||||
configured_runtime_dir, run_vm,
|
||||
};
|
||||
@@ -1,782 +1,11 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::Parser;
|
||||
use futures::Stream;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use openshell_core::VERSION;
|
||||
use openshell_core::proto::compute::v1::compute_driver_server::ComputeDriverServer;
|
||||
use openshell_driver_vm::otel_tracing::compute_driver_rpc_layer;
|
||||
#[cfg(target_os = "macos")]
|
||||
use openshell_driver_vm::{VM_RUNTIME_DIR_ENV, configured_runtime_dir};
|
||||
use openshell_driver_vm::{VmBackend, VmDriver, VmDriverConfig, VmLaunchConfig, procguard, run_vm};
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "openshell-driver-vm")]
|
||||
#[command(version = VERSION)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct Args {
|
||||
#[arg(long, hide = true, default_value_t = false)]
|
||||
internal_run_vm: bool,
|
||||
|
||||
#[arg(long = "vm-root-disk", hide = true, alias = "vm-rootfs")]
|
||||
vm_root_disk: Option<PathBuf>,
|
||||
|
||||
#[arg(long = "vm-overlay-disk", hide = true)]
|
||||
vm_overlay_disk: Option<PathBuf>,
|
||||
|
||||
#[arg(long = "vm-image-disk", hide = true)]
|
||||
vm_image_disk: Option<PathBuf>,
|
||||
|
||||
#[arg(long = "vm-kernel-image", hide = true)]
|
||||
vm_kernel_image: Option<PathBuf>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_exec: Option<String>,
|
||||
|
||||
#[arg(long, hide = true, default_value = "/")]
|
||||
vm_workdir: String,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_env: Vec<String>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_console_output: Option<PathBuf>,
|
||||
|
||||
#[arg(long, hide = true, default_value_t = 2)]
|
||||
vm_vcpus: u8,
|
||||
|
||||
#[arg(long, hide = true, default_value_t = 2048)]
|
||||
vm_mem_mib: u32,
|
||||
|
||||
#[arg(long, hide = true, default_value_t = 1)]
|
||||
vm_krun_log_level: u32,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_COMPUTE_DRIVER_BIND")]
|
||||
bind_address: Option<SocketAddr>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_COMPUTE_DRIVER_SOCKET")]
|
||||
bind_socket: Option<PathBuf>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
expected_peer_pid: Option<u32>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_COMPUTE_DRIVER_ALLOW_UNAUTHENTICATED_TCP",
|
||||
default_value_t = false
|
||||
)]
|
||||
allow_unauthenticated_tcp: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_COMPUTE_DRIVER_ALLOW_SAME_UID_PEER",
|
||||
default_value_t = false
|
||||
)]
|
||||
allow_same_uid_peer: bool,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_LOG_LEVEL", default_value = "info")]
|
||||
log_level: String,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_OTLP_ENDPOINT")]
|
||||
otlp_endpoint: Option<String>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_GRPC_ENDPOINT")]
|
||||
openshell_endpoint: Option<String>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_IMAGE", default_value = "")]
|
||||
default_image: String,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_BOOTSTRAP_IMAGE", default_value = "")]
|
||||
bootstrap_image: String,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_VM_DRIVER_STATE_DIR",
|
||||
default_value = "target/openshell-vm-driver"
|
||||
)]
|
||||
state_dir: PathBuf,
|
||||
|
||||
#[arg(long = "guest-tls-ca", env = "OPENSHELL_VM_TLS_CA")]
|
||||
guest_tls_ca: Option<PathBuf>,
|
||||
|
||||
#[arg(long = "guest-tls-cert", env = "OPENSHELL_VM_TLS_CERT")]
|
||||
guest_tls_cert: Option<PathBuf>,
|
||||
|
||||
#[arg(long = "guest-tls-key", env = "OPENSHELL_VM_TLS_KEY")]
|
||||
guest_tls_key: Option<PathBuf>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_KRUN_LOG_LEVEL", default_value_t = 1)]
|
||||
krun_log_level: u32,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_DRIVER_VCPUS", default_value_t = 2)]
|
||||
vcpus: u8,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_DRIVER_MEM_MIB", default_value_t = 2048)]
|
||||
mem_mib: u32,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_OVERLAY_DISK_MIB", default_value_t = 4096)]
|
||||
overlay_disk_mib: u64,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_GPU")]
|
||||
gpu: bool,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_GPU_MEM_MIB", default_value_t = 8192)]
|
||||
gpu_mem_mib: u32,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_GPU_VCPUS", default_value_t = 4)]
|
||||
gpu_vcpus: u8,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_SANDBOX_UID")]
|
||||
sandbox_uid: Option<u32>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_SANDBOX_GID")]
|
||||
sandbox_gid: Option<u32>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_backend: Option<String>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_gpu_bdf: Option<String>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_tap_device: Option<String>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_guest_ip: Option<String>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_host_ip: Option<String>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_vsock_cid: Option<u32>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_guest_mac: Option<String>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_gateway_port: Option<u16>,
|
||||
#[cfg(target_os = "windows")]
|
||||
fn main() {
|
||||
eprintln!("openshell-driver-vm is unsupported on Windows");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
if args.internal_run_vm {
|
||||
// We intentionally defer procguard arming until `run_vm()` so
|
||||
// that the only arm is the one that knows how to clean up
|
||||
// gvproxy. Racing two watchers against the same parent-death
|
||||
// event causes the bare arm's `exit(1)` to win, skipping the
|
||||
// gvproxy cleanup and leaking the helper. The risk window
|
||||
// before `run_vm` arms procguard is ~a few syscalls long
|
||||
// (`build_vm_launch_config`, `configured_runtime_dir`), which
|
||||
// is negligible next to the parent gRPC server's uptime.
|
||||
maybe_reexec_internal_vm_with_runtime_env()?;
|
||||
let config = build_vm_launch_config(&args).map_err(|err| miette::miette!("{err}"))?;
|
||||
run_vm(&config).map_err(|err| miette::miette!("{err}"))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (tracer_provider, setup_error) =
|
||||
openshell_driver_vm::otel_tracing::provider_for(args.otlp_endpoint.as_deref());
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)))
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.with(
|
||||
tracer_provider
|
||||
.as_ref()
|
||||
.map(openshell_driver_vm::otel_tracing::layer),
|
||||
)
|
||||
.init();
|
||||
if let Some(error) = setup_error {
|
||||
tracing::error!(%error, "OTLP exporting could not be started");
|
||||
} else if let Some(endpoint) = &args.otlp_endpoint {
|
||||
info!(endpoint, "OTLP exporting enabled");
|
||||
}
|
||||
|
||||
let listen_mode = compute_driver_listen_mode(&args).map_err(|err| miette::miette!("{err}"))?;
|
||||
|
||||
// Arm procguard so that if the gateway is killed (SIGKILL or crash)
|
||||
// we also die. Without this the driver is reparented to init and
|
||||
// keeps its per-sandbox VM launchers alive forever. Launchers have
|
||||
// their own procguards (armed in `run_vm`) which cascade cleanup of
|
||||
// gvproxy and the libkrun worker the moment this driver exits.
|
||||
if let Err(err) = procguard::die_with_parent() {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"procguard arm failed; gateway crashes may orphan this driver"
|
||||
);
|
||||
}
|
||||
|
||||
let driver = VmDriver::new(VmDriverConfig {
|
||||
openshell_endpoint: args
|
||||
.openshell_endpoint
|
||||
.ok_or_else(|| miette::miette!("OPENSHELL_GRPC_ENDPOINT is required"))?,
|
||||
state_dir: args.state_dir.clone(),
|
||||
launcher_bin: None,
|
||||
default_image: args.default_image.clone(),
|
||||
bootstrap_image: args.bootstrap_image.clone(),
|
||||
log_level: args.log_level.clone(),
|
||||
krun_log_level: args.krun_log_level,
|
||||
vcpus: args.vcpus,
|
||||
mem_mib: args.mem_mib,
|
||||
overlay_disk_mib: args.overlay_disk_mib,
|
||||
guest_tls_ca: args.guest_tls_ca.clone(),
|
||||
guest_tls_cert: args.guest_tls_cert.clone(),
|
||||
guest_tls_key: args.guest_tls_key.clone(),
|
||||
gpu_enabled: args.gpu,
|
||||
gpu_mem_mib: args.gpu_mem_mib,
|
||||
gpu_vcpus: args.gpu_vcpus,
|
||||
sandbox_uid: args.sandbox_uid,
|
||||
sandbox_gid: args.sandbox_gid,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| miette::miette!("{err}"))?;
|
||||
|
||||
let result = match listen_mode {
|
||||
ComputeDriverListenMode::Unix {
|
||||
socket_path,
|
||||
expected_peer_pid,
|
||||
} => {
|
||||
prepare_compute_driver_socket(&socket_path).map_err(|err| miette::miette!("{err}"))?;
|
||||
|
||||
info!(socket = %socket_path.display(), "Starting vm compute driver");
|
||||
let listener = UnixListener::bind(&socket_path).into_diagnostic()?;
|
||||
restrict_socket_permissions(&socket_path).map_err(|err| miette::miette!("{err}"))?;
|
||||
let result = tonic::transport::Server::builder()
|
||||
.layer(compute_driver_rpc_layer())
|
||||
.add_service(ComputeDriverServer::new(driver))
|
||||
.serve_with_incoming_shutdown(
|
||||
AuthenticatedUnixIncoming::new(listener, expected_peer_pid),
|
||||
shutdown_signal(),
|
||||
)
|
||||
.await
|
||||
.into_diagnostic();
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
result
|
||||
}
|
||||
ComputeDriverListenMode::Tcp(bind_address) => {
|
||||
info!(address = %bind_address, "Starting unauthenticated dev vm compute driver");
|
||||
tonic::transport::Server::builder()
|
||||
.layer(compute_driver_rpc_layer())
|
||||
.add_service(ComputeDriverServer::new(driver))
|
||||
.serve_with_shutdown(bind_address, shutdown_signal())
|
||||
.await
|
||||
.into_diagnostic()
|
||||
}
|
||||
};
|
||||
if let Some(provider) = &tracer_provider
|
||||
&& let Err(error) = provider.shutdown()
|
||||
{
|
||||
tracing::warn!(%error, "OTLP tracer provider shutdown failed");
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.expect("install SIGTERM handler");
|
||||
tokio::select! {
|
||||
result = tokio::signal::ctrl_c() => {
|
||||
if let Err(error) = result {
|
||||
tracing::warn!(%error, "failed to listen for Ctrl-C");
|
||||
}
|
||||
}
|
||||
_ = terminate.recv() => {}
|
||||
}
|
||||
info!("Shutdown signal received; stopping vm compute driver");
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum ComputeDriverListenMode {
|
||||
Unix {
|
||||
socket_path: PathBuf,
|
||||
expected_peer_pid: Option<u32>,
|
||||
},
|
||||
Tcp(SocketAddr),
|
||||
}
|
||||
|
||||
fn compute_driver_listen_mode(args: &Args) -> std::result::Result<ComputeDriverListenMode, String> {
|
||||
if let Some(socket_path) = args.bind_socket.clone() {
|
||||
if args.expected_peer_pid.is_none() && !args.allow_same_uid_peer {
|
||||
return Err(
|
||||
"--expected-peer-pid is required with --bind-socket; use --allow-same-uid-peer only for local development"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
return Ok(ComputeDriverListenMode::Unix {
|
||||
socket_path,
|
||||
expected_peer_pid: args.expected_peer_pid,
|
||||
});
|
||||
}
|
||||
|
||||
if !args.allow_unauthenticated_tcp {
|
||||
return Err(
|
||||
"--bind-socket is required; unauthenticated TCP mode is disabled unless --allow-unauthenticated-tcp is set for local development"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let Some(bind_address) = args.bind_address else {
|
||||
return Err("--bind-address is required with --allow-unauthenticated-tcp".to_string());
|
||||
};
|
||||
|
||||
Ok(ComputeDriverListenMode::Tcp(bind_address))
|
||||
}
|
||||
|
||||
fn prepare_compute_driver_socket(socket_path: &Path) -> std::result::Result<(), String> {
|
||||
let Some(parent) = socket_path.parent() else {
|
||||
return Err(format!(
|
||||
"vm compute driver socket path '{}' has no parent directory",
|
||||
socket_path.display()
|
||||
));
|
||||
};
|
||||
let expected_uid = current_euid();
|
||||
prepare_private_socket_dir(parent, expected_uid)?;
|
||||
remove_stale_socket(socket_path, expected_uid)
|
||||
}
|
||||
|
||||
fn current_euid() -> u32 {
|
||||
rustix::process::geteuid().as_raw()
|
||||
}
|
||||
|
||||
fn prepare_private_socket_dir(
|
||||
socket_dir: &Path,
|
||||
expected_uid: u32,
|
||||
) -> std::result::Result<(), String> {
|
||||
std::fs::create_dir_all(socket_dir)
|
||||
.map_err(|err| format!("create socket dir {}: {err}", socket_dir.display()))?;
|
||||
let metadata = std::fs::symlink_metadata(socket_dir)
|
||||
.map_err(|err| format!("stat socket dir {}: {err}", socket_dir.display()))?;
|
||||
let file_type = metadata.file_type();
|
||||
if file_type.is_symlink() {
|
||||
return Err(format!(
|
||||
"socket dir {} is a symlink; refusing to use it",
|
||||
socket_dir.display()
|
||||
));
|
||||
}
|
||||
if !file_type.is_dir() {
|
||||
return Err(format!(
|
||||
"socket dir {} is not a directory",
|
||||
socket_dir.display()
|
||||
));
|
||||
}
|
||||
if metadata.uid() != expected_uid {
|
||||
return Err(format!(
|
||||
"socket dir {} is owned by uid {} but current euid is {}",
|
||||
socket_dir.display(),
|
||||
metadata.uid(),
|
||||
expected_uid
|
||||
));
|
||||
}
|
||||
std::fs::set_permissions(socket_dir, std::fs::Permissions::from_mode(0o700))
|
||||
.map_err(|err| format!("chmod socket dir {}: {err}", socket_dir.display()))
|
||||
}
|
||||
|
||||
fn remove_stale_socket(socket_path: &Path, expected_uid: u32) -> std::result::Result<(), String> {
|
||||
let metadata = match std::fs::symlink_metadata(socket_path) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
|
||||
Err(err) => return Err(format!("stat socket {}: {err}", socket_path.display())),
|
||||
};
|
||||
let file_type = metadata.file_type();
|
||||
if file_type.is_symlink() {
|
||||
return Err(format!(
|
||||
"socket {} is a symlink; refusing to remove it",
|
||||
socket_path.display()
|
||||
));
|
||||
}
|
||||
if metadata.uid() != expected_uid {
|
||||
return Err(format!(
|
||||
"socket {} is owned by uid {} but current euid is {}",
|
||||
socket_path.display(),
|
||||
metadata.uid(),
|
||||
expected_uid
|
||||
));
|
||||
}
|
||||
if !file_type.is_socket() {
|
||||
return Err(format!(
|
||||
"socket path {} exists but is not a Unix socket",
|
||||
socket_path.display()
|
||||
));
|
||||
}
|
||||
std::fs::remove_file(socket_path)
|
||||
.map_err(|err| format!("remove stale socket {}: {err}", socket_path.display()))
|
||||
}
|
||||
|
||||
fn restrict_socket_permissions(socket_path: &Path) -> std::result::Result<(), String> {
|
||||
std::fs::set_permissions(socket_path, std::fs::Permissions::from_mode(0o600))
|
||||
.map_err(|err| format!("chmod socket {}: {err}", socket_path.display()))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct PeerCredentials {
|
||||
uid: u32,
|
||||
pid: Option<i32>,
|
||||
}
|
||||
|
||||
fn peer_credentials(stream: &UnixStream) -> std::result::Result<PeerCredentials, String> {
|
||||
let credentials = stream
|
||||
.peer_cred()
|
||||
.map_err(|err| format!("read peer credentials: {err}"))?;
|
||||
Ok(PeerCredentials {
|
||||
uid: credentials.uid(),
|
||||
pid: credentials.pid(),
|
||||
})
|
||||
}
|
||||
|
||||
fn authorize_peer_credentials(
|
||||
peer: PeerCredentials,
|
||||
driver_uid: u32,
|
||||
gateway_pid: Option<u32>,
|
||||
) -> std::result::Result<(), String> {
|
||||
if peer.uid != driver_uid {
|
||||
return Err(format!(
|
||||
"peer uid {} does not match current euid {}",
|
||||
peer.uid, driver_uid
|
||||
));
|
||||
}
|
||||
let Some(gateway_pid) = gateway_pid else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(peer_process_id) = peer.pid.and_then(|pid| u32::try_from(pid).ok()) else {
|
||||
return Err(format!(
|
||||
"peer pid is unavailable; expected gateway pid {gateway_pid}"
|
||||
));
|
||||
};
|
||||
if peer_process_id != gateway_pid {
|
||||
return Err(format!(
|
||||
"peer pid {peer_process_id} does not match expected gateway pid {gateway_pid}"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct AuthenticatedUnixIncoming {
|
||||
listener: UnixListener,
|
||||
expected_uid: u32,
|
||||
expected_peer_pid: Option<u32>,
|
||||
}
|
||||
|
||||
impl AuthenticatedUnixIncoming {
|
||||
fn new(listener: UnixListener, expected_peer_pid: Option<u32>) -> Self {
|
||||
Self {
|
||||
listener,
|
||||
expected_uid: current_euid(),
|
||||
expected_peer_pid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for AuthenticatedUnixIncoming {
|
||||
type Item = io::Result<UnixStream>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
loop {
|
||||
match this.listener.poll_accept(cx) {
|
||||
Poll::Ready(Ok((stream, _addr))) => {
|
||||
let authorized = peer_credentials(&stream).and_then(|peer| {
|
||||
authorize_peer_credentials(peer, this.expected_uid, this.expected_peer_pid)
|
||||
});
|
||||
match authorized {
|
||||
Ok(()) => return Poll::Ready(Some(Ok(stream))),
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"rejected vm compute driver UDS client"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Poll::Ready(Err(err)) => return Poll::Ready(Some(Err(err))),
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_vm_launch_config(args: &Args) -> std::result::Result<VmLaunchConfig, String> {
|
||||
let root_disk = args
|
||||
.vm_root_disk
|
||||
.clone()
|
||||
.ok_or_else(|| "--vm-root-disk is required in internal VM mode".to_string())?;
|
||||
let overlay_disk = args
|
||||
.vm_overlay_disk
|
||||
.clone()
|
||||
.ok_or_else(|| "--vm-overlay-disk is required in internal VM mode".to_string())?;
|
||||
let image_disk = args.vm_image_disk.clone();
|
||||
let exec_path = args
|
||||
.vm_exec
|
||||
.clone()
|
||||
.ok_or_else(|| "--vm-exec is required in internal VM mode".to_string())?;
|
||||
let console_output = args
|
||||
.vm_console_output
|
||||
.clone()
|
||||
.ok_or_else(|| "--vm-console-output is required in internal VM mode".to_string())?;
|
||||
|
||||
let backend = match args.vm_backend.as_deref() {
|
||||
Some("qemu") => VmBackend::Qemu,
|
||||
Some("libkrun") | None => VmBackend::Libkrun,
|
||||
Some(other) => return Err(format!("unknown VM backend: {other}")),
|
||||
};
|
||||
|
||||
Ok(VmLaunchConfig {
|
||||
root_disk,
|
||||
overlay_disk,
|
||||
image_disk,
|
||||
kernel_image: args.vm_kernel_image.clone(),
|
||||
vcpus: args.vm_vcpus,
|
||||
mem_mib: args.vm_mem_mib,
|
||||
exec_path,
|
||||
args: Vec::new(),
|
||||
env: args.vm_env.clone(),
|
||||
workdir: args.vm_workdir.clone(),
|
||||
log_level: args.vm_krun_log_level,
|
||||
console_output,
|
||||
backend,
|
||||
gpu_bdf: args.vm_gpu_bdf.clone(),
|
||||
tap_device: args.vm_tap_device.clone(),
|
||||
guest_ip: args.vm_guest_ip.clone(),
|
||||
host_ip: args.vm_host_ip.clone(),
|
||||
vsock_cid: args.vm_vsock_cid,
|
||||
guest_mac: args.vm_guest_mac.clone(),
|
||||
gateway_port: args.vm_gateway_port,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn maybe_reexec_internal_vm_with_runtime_env() -> Result<()> {
|
||||
use std::os::unix::process::CommandExt as _;
|
||||
|
||||
const REEXEC_ENV: &str = "__OPENSHELL_DRIVER_VM_REEXEC";
|
||||
|
||||
if std::env::var_os(REEXEC_ENV).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let runtime_dir = configured_runtime_dir().map_err(|err| miette::miette!("{err}"))?;
|
||||
let runtime_str = runtime_dir.to_string_lossy();
|
||||
let needs_reexec = std::env::var_os("DYLD_LIBRARY_PATH")
|
||||
.is_none_or(|value| !value.to_string_lossy().contains(runtime_str.as_ref()));
|
||||
if !needs_reexec {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut dyld_paths = vec![runtime_dir.clone()];
|
||||
if let Some(existing) = std::env::var_os("DYLD_LIBRARY_PATH") {
|
||||
dyld_paths.extend(std::env::split_paths(&existing));
|
||||
}
|
||||
let joined = std::env::join_paths(&dyld_paths)
|
||||
.map_err(|err| miette::miette!("join DYLD_LIBRARY_PATH: {err}"))?;
|
||||
let exe = std::env::current_exe().into_diagnostic()?;
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
|
||||
// Use execvp() so the current process is *replaced* by the re-exec'd
|
||||
// binary — no wrapper process sits between the compute driver and
|
||||
// the actually-running VM launcher. That avoids two problems:
|
||||
// 1. An extra process level that survives SIGKILL of the driver
|
||||
// (the wrapper was reparenting the re-exec'd child to init).
|
||||
// 2. Signal forwarding: with a wrapper, a SIGTERM to the wrapper
|
||||
// doesn't reach the child unless we hand-roll forwarding.
|
||||
// After exec, the child inherits our PID and our procguard arming.
|
||||
let err = std::process::Command::new(exe)
|
||||
.args(&args)
|
||||
.env("DYLD_LIBRARY_PATH", &joined)
|
||||
.env(VM_RUNTIME_DIR_ENV, runtime_dir)
|
||||
.env(REEXEC_ENV, "1")
|
||||
.exec();
|
||||
// `exec()` only returns on failure.
|
||||
Err(miette::miette!("failed to re-exec with runtime env: {err}"))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
// Signature must match the macOS variant which can fail.
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn maybe_reexec_internal_vm_with_runtime_env() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
Args, ComputeDriverListenMode, PeerCredentials, authorize_peer_credentials,
|
||||
compute_driver_listen_mode,
|
||||
};
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn peer_authorization_accepts_matching_uid_and_pid() {
|
||||
authorize_peer_credentials(
|
||||
PeerCredentials {
|
||||
uid: 1000,
|
||||
pid: Some(42),
|
||||
},
|
||||
1000,
|
||||
Some(42),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_authorization_rejects_wrong_pid() {
|
||||
let err = authorize_peer_credentials(
|
||||
PeerCredentials {
|
||||
uid: 1000,
|
||||
pid: Some(7),
|
||||
},
|
||||
1000,
|
||||
Some(42),
|
||||
)
|
||||
.expect_err("wrong pid should be rejected");
|
||||
assert!(err.contains("does not match expected gateway pid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_authorization_rejects_wrong_uid() {
|
||||
let err = authorize_peer_credentials(
|
||||
PeerCredentials {
|
||||
uid: 1001,
|
||||
pid: Some(42),
|
||||
},
|
||||
1000,
|
||||
Some(42),
|
||||
)
|
||||
.expect_err("wrong uid should be rejected");
|
||||
assert!(err.contains("does not match current euid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_authorization_rejects_missing_pid_when_expected() {
|
||||
let err = authorize_peer_credentials(
|
||||
PeerCredentials {
|
||||
uid: 1000,
|
||||
pid: None,
|
||||
},
|
||||
1000,
|
||||
Some(42),
|
||||
)
|
||||
.expect_err("missing pid should be rejected");
|
||||
assert!(err.contains("peer pid is unavailable"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_authorization_accepts_matching_uid_without_expected_pid() {
|
||||
authorize_peer_credentials(
|
||||
PeerCredentials {
|
||||
uid: 1000,
|
||||
pid: None,
|
||||
},
|
||||
1000,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listen_mode_rejects_default_tcp() {
|
||||
let args = Args::parse_from(["openshell-driver-vm"]);
|
||||
let err = compute_driver_listen_mode(&args).expect_err("default TCP should be disabled");
|
||||
assert!(err.contains("--bind-socket is required"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_gateway_otlp_endpoint() {
|
||||
let args = Args::try_parse_from([
|
||||
"openshell-driver-vm",
|
||||
"--otlp-endpoint",
|
||||
"http://127.0.0.1:4317",
|
||||
]);
|
||||
assert!(
|
||||
args.is_ok(),
|
||||
"VM driver should accept the gateway OTLP endpoint"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listen_mode_rejects_bind_address_without_tcp_opt_in() {
|
||||
let args = Args::parse_from(["openshell-driver-vm", "--bind-address", "127.0.0.1:50061"]);
|
||||
let err =
|
||||
compute_driver_listen_mode(&args).expect_err("TCP bind should require explicit opt-in");
|
||||
assert!(err.contains("--allow-unauthenticated-tcp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listen_mode_requires_bind_address_with_tcp_opt_in() {
|
||||
let args = Args::parse_from(["openshell-driver-vm", "--allow-unauthenticated-tcp"]);
|
||||
let err =
|
||||
compute_driver_listen_mode(&args).expect_err("TCP opt-in should require an address");
|
||||
assert!(err.contains("--bind-address is required"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listen_mode_accepts_explicit_unauthenticated_tcp() {
|
||||
let args = Args::parse_from([
|
||||
"openshell-driver-vm",
|
||||
"--allow-unauthenticated-tcp",
|
||||
"--bind-address",
|
||||
"127.0.0.1:50061",
|
||||
]);
|
||||
assert_eq!(
|
||||
compute_driver_listen_mode(&args).unwrap(),
|
||||
ComputeDriverListenMode::Tcp("127.0.0.1:50061".parse().unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listen_mode_requires_expected_peer_pid_for_uds() {
|
||||
let args = Args::parse_from([
|
||||
"openshell-driver-vm",
|
||||
"--bind-socket",
|
||||
"/tmp/compute-driver.sock",
|
||||
]);
|
||||
let err = compute_driver_listen_mode(&args)
|
||||
.expect_err("UDS should require gateway peer pid by default");
|
||||
assert!(err.contains("--expected-peer-pid is required"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listen_mode_accepts_uds_with_expected_peer_pid() {
|
||||
let args = Args::parse_from([
|
||||
"openshell-driver-vm",
|
||||
"--bind-socket",
|
||||
"/tmp/compute-driver.sock",
|
||||
"--expected-peer-pid",
|
||||
"42",
|
||||
]);
|
||||
assert_eq!(
|
||||
compute_driver_listen_mode(&args).unwrap(),
|
||||
ComputeDriverListenMode::Unix {
|
||||
socket_path: PathBuf::from("/tmp/compute-driver.sock"),
|
||||
expected_peer_pid: Some(42),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listen_mode_accepts_explicit_same_uid_uds_dev_mode() {
|
||||
let args = Args::parse_from([
|
||||
"openshell-driver-vm",
|
||||
"--bind-socket",
|
||||
"/tmp/compute-driver.sock",
|
||||
"--allow-same-uid-peer",
|
||||
]);
|
||||
assert_eq!(
|
||||
compute_driver_listen_mode(&args).unwrap(),
|
||||
ComputeDriverListenMode::Unix {
|
||||
socket_path: PathBuf::from("/tmp/compute-driver.sock"),
|
||||
expected_peer_pid: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
include!("main_unix.rs");
|
||||
|
||||
@@ -0,0 +1,782 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::Parser;
|
||||
use futures::Stream;
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use openshell_core::VERSION;
|
||||
use openshell_core::proto::compute::v1::compute_driver_server::ComputeDriverServer;
|
||||
use openshell_driver_vm::otel_tracing::compute_driver_rpc_layer;
|
||||
#[cfg(target_os = "macos")]
|
||||
use openshell_driver_vm::{VM_RUNTIME_DIR_ENV, configured_runtime_dir};
|
||||
use openshell_driver_vm::{VmBackend, VmDriver, VmDriverConfig, VmLaunchConfig, procguard, run_vm};
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "openshell-driver-vm")]
|
||||
#[command(version = VERSION)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct Args {
|
||||
#[arg(long, hide = true, default_value_t = false)]
|
||||
internal_run_vm: bool,
|
||||
|
||||
#[arg(long = "vm-root-disk", hide = true, alias = "vm-rootfs")]
|
||||
vm_root_disk: Option<PathBuf>,
|
||||
|
||||
#[arg(long = "vm-overlay-disk", hide = true)]
|
||||
vm_overlay_disk: Option<PathBuf>,
|
||||
|
||||
#[arg(long = "vm-image-disk", hide = true)]
|
||||
vm_image_disk: Option<PathBuf>,
|
||||
|
||||
#[arg(long = "vm-kernel-image", hide = true)]
|
||||
vm_kernel_image: Option<PathBuf>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_exec: Option<String>,
|
||||
|
||||
#[arg(long, hide = true, default_value = "/")]
|
||||
vm_workdir: String,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_env: Vec<String>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_console_output: Option<PathBuf>,
|
||||
|
||||
#[arg(long, hide = true, default_value_t = 2)]
|
||||
vm_vcpus: u8,
|
||||
|
||||
#[arg(long, hide = true, default_value_t = 2048)]
|
||||
vm_mem_mib: u32,
|
||||
|
||||
#[arg(long, hide = true, default_value_t = 1)]
|
||||
vm_krun_log_level: u32,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_COMPUTE_DRIVER_BIND")]
|
||||
bind_address: Option<SocketAddr>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_COMPUTE_DRIVER_SOCKET")]
|
||||
bind_socket: Option<PathBuf>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
expected_peer_pid: Option<u32>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_COMPUTE_DRIVER_ALLOW_UNAUTHENTICATED_TCP",
|
||||
default_value_t = false
|
||||
)]
|
||||
allow_unauthenticated_tcp: bool,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_COMPUTE_DRIVER_ALLOW_SAME_UID_PEER",
|
||||
default_value_t = false
|
||||
)]
|
||||
allow_same_uid_peer: bool,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_LOG_LEVEL", default_value = "info")]
|
||||
log_level: String,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_OTLP_ENDPOINT")]
|
||||
otlp_endpoint: Option<String>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_GRPC_ENDPOINT")]
|
||||
openshell_endpoint: Option<String>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_SANDBOX_IMAGE", default_value = "")]
|
||||
default_image: String,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_BOOTSTRAP_IMAGE", default_value = "")]
|
||||
bootstrap_image: String,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
env = "OPENSHELL_VM_DRIVER_STATE_DIR",
|
||||
default_value = "target/openshell-vm-driver"
|
||||
)]
|
||||
state_dir: PathBuf,
|
||||
|
||||
#[arg(long = "guest-tls-ca", env = "OPENSHELL_VM_TLS_CA")]
|
||||
guest_tls_ca: Option<PathBuf>,
|
||||
|
||||
#[arg(long = "guest-tls-cert", env = "OPENSHELL_VM_TLS_CERT")]
|
||||
guest_tls_cert: Option<PathBuf>,
|
||||
|
||||
#[arg(long = "guest-tls-key", env = "OPENSHELL_VM_TLS_KEY")]
|
||||
guest_tls_key: Option<PathBuf>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_KRUN_LOG_LEVEL", default_value_t = 1)]
|
||||
krun_log_level: u32,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_DRIVER_VCPUS", default_value_t = 2)]
|
||||
vcpus: u8,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_DRIVER_MEM_MIB", default_value_t = 2048)]
|
||||
mem_mib: u32,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_OVERLAY_DISK_MIB", default_value_t = 4096)]
|
||||
overlay_disk_mib: u64,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_GPU")]
|
||||
gpu: bool,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_GPU_MEM_MIB", default_value_t = 8192)]
|
||||
gpu_mem_mib: u32,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_GPU_VCPUS", default_value_t = 4)]
|
||||
gpu_vcpus: u8,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_SANDBOX_UID")]
|
||||
sandbox_uid: Option<u32>,
|
||||
|
||||
#[arg(long, env = "OPENSHELL_VM_SANDBOX_GID")]
|
||||
sandbox_gid: Option<u32>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_backend: Option<String>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_gpu_bdf: Option<String>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_tap_device: Option<String>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_guest_ip: Option<String>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_host_ip: Option<String>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_vsock_cid: Option<u32>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_guest_mac: Option<String>,
|
||||
|
||||
#[arg(long, hide = true)]
|
||||
vm_gateway_port: Option<u16>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
if args.internal_run_vm {
|
||||
// We intentionally defer procguard arming until `run_vm()` so
|
||||
// that the only arm is the one that knows how to clean up
|
||||
// gvproxy. Racing two watchers against the same parent-death
|
||||
// event causes the bare arm's `exit(1)` to win, skipping the
|
||||
// gvproxy cleanup and leaking the helper. The risk window
|
||||
// before `run_vm` arms procguard is ~a few syscalls long
|
||||
// (`build_vm_launch_config`, `configured_runtime_dir`), which
|
||||
// is negligible next to the parent gRPC server's uptime.
|
||||
maybe_reexec_internal_vm_with_runtime_env()?;
|
||||
let config = build_vm_launch_config(&args).map_err(|err| miette::miette!("{err}"))?;
|
||||
run_vm(&config).map_err(|err| miette::miette!("{err}"))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (tracer_provider, setup_error) =
|
||||
openshell_driver_vm::otel_tracing::provider_for(args.otlp_endpoint.as_deref());
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&args.log_level)))
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.with(
|
||||
tracer_provider
|
||||
.as_ref()
|
||||
.map(openshell_driver_vm::otel_tracing::layer),
|
||||
)
|
||||
.init();
|
||||
if let Some(error) = setup_error {
|
||||
tracing::error!(%error, "OTLP exporting could not be started");
|
||||
} else if let Some(endpoint) = &args.otlp_endpoint {
|
||||
info!(endpoint, "OTLP exporting enabled");
|
||||
}
|
||||
|
||||
let listen_mode = compute_driver_listen_mode(&args).map_err(|err| miette::miette!("{err}"))?;
|
||||
|
||||
// Arm procguard so that if the gateway is killed (SIGKILL or crash)
|
||||
// we also die. Without this the driver is reparented to init and
|
||||
// keeps its per-sandbox VM launchers alive forever. Launchers have
|
||||
// their own procguards (armed in `run_vm`) which cascade cleanup of
|
||||
// gvproxy and the libkrun worker the moment this driver exits.
|
||||
if let Err(err) = procguard::die_with_parent() {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"procguard arm failed; gateway crashes may orphan this driver"
|
||||
);
|
||||
}
|
||||
|
||||
let driver = VmDriver::new(VmDriverConfig {
|
||||
openshell_endpoint: args
|
||||
.openshell_endpoint
|
||||
.ok_or_else(|| miette::miette!("OPENSHELL_GRPC_ENDPOINT is required"))?,
|
||||
state_dir: args.state_dir.clone(),
|
||||
launcher_bin: None,
|
||||
default_image: args.default_image.clone(),
|
||||
bootstrap_image: args.bootstrap_image.clone(),
|
||||
log_level: args.log_level.clone(),
|
||||
krun_log_level: args.krun_log_level,
|
||||
vcpus: args.vcpus,
|
||||
mem_mib: args.mem_mib,
|
||||
overlay_disk_mib: args.overlay_disk_mib,
|
||||
guest_tls_ca: args.guest_tls_ca.clone(),
|
||||
guest_tls_cert: args.guest_tls_cert.clone(),
|
||||
guest_tls_key: args.guest_tls_key.clone(),
|
||||
gpu_enabled: args.gpu,
|
||||
gpu_mem_mib: args.gpu_mem_mib,
|
||||
gpu_vcpus: args.gpu_vcpus,
|
||||
sandbox_uid: args.sandbox_uid,
|
||||
sandbox_gid: args.sandbox_gid,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| miette::miette!("{err}"))?;
|
||||
|
||||
let result = match listen_mode {
|
||||
ComputeDriverListenMode::Unix {
|
||||
socket_path,
|
||||
expected_peer_pid,
|
||||
} => {
|
||||
prepare_compute_driver_socket(&socket_path).map_err(|err| miette::miette!("{err}"))?;
|
||||
|
||||
info!(socket = %socket_path.display(), "Starting vm compute driver");
|
||||
let listener = UnixListener::bind(&socket_path).into_diagnostic()?;
|
||||
restrict_socket_permissions(&socket_path).map_err(|err| miette::miette!("{err}"))?;
|
||||
let result = tonic::transport::Server::builder()
|
||||
.layer(compute_driver_rpc_layer())
|
||||
.add_service(ComputeDriverServer::new(driver))
|
||||
.serve_with_incoming_shutdown(
|
||||
AuthenticatedUnixIncoming::new(listener, expected_peer_pid),
|
||||
shutdown_signal(),
|
||||
)
|
||||
.await
|
||||
.into_diagnostic();
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
result
|
||||
}
|
||||
ComputeDriverListenMode::Tcp(bind_address) => {
|
||||
info!(address = %bind_address, "Starting unauthenticated dev vm compute driver");
|
||||
tonic::transport::Server::builder()
|
||||
.layer(compute_driver_rpc_layer())
|
||||
.add_service(ComputeDriverServer::new(driver))
|
||||
.serve_with_shutdown(bind_address, shutdown_signal())
|
||||
.await
|
||||
.into_diagnostic()
|
||||
}
|
||||
};
|
||||
if let Some(provider) = &tracer_provider
|
||||
&& let Err(error) = provider.shutdown()
|
||||
{
|
||||
tracing::warn!(%error, "OTLP tracer provider shutdown failed");
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.expect("install SIGTERM handler");
|
||||
tokio::select! {
|
||||
result = tokio::signal::ctrl_c() => {
|
||||
if let Err(error) = result {
|
||||
tracing::warn!(%error, "failed to listen for Ctrl-C");
|
||||
}
|
||||
}
|
||||
_ = terminate.recv() => {}
|
||||
}
|
||||
info!("Shutdown signal received; stopping vm compute driver");
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum ComputeDriverListenMode {
|
||||
Unix {
|
||||
socket_path: PathBuf,
|
||||
expected_peer_pid: Option<u32>,
|
||||
},
|
||||
Tcp(SocketAddr),
|
||||
}
|
||||
|
||||
fn compute_driver_listen_mode(args: &Args) -> std::result::Result<ComputeDriverListenMode, String> {
|
||||
if let Some(socket_path) = args.bind_socket.clone() {
|
||||
if args.expected_peer_pid.is_none() && !args.allow_same_uid_peer {
|
||||
return Err(
|
||||
"--expected-peer-pid is required with --bind-socket; use --allow-same-uid-peer only for local development"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
return Ok(ComputeDriverListenMode::Unix {
|
||||
socket_path,
|
||||
expected_peer_pid: args.expected_peer_pid,
|
||||
});
|
||||
}
|
||||
|
||||
if !args.allow_unauthenticated_tcp {
|
||||
return Err(
|
||||
"--bind-socket is required; unauthenticated TCP mode is disabled unless --allow-unauthenticated-tcp is set for local development"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let Some(bind_address) = args.bind_address else {
|
||||
return Err("--bind-address is required with --allow-unauthenticated-tcp".to_string());
|
||||
};
|
||||
|
||||
Ok(ComputeDriverListenMode::Tcp(bind_address))
|
||||
}
|
||||
|
||||
fn prepare_compute_driver_socket(socket_path: &Path) -> std::result::Result<(), String> {
|
||||
let Some(parent) = socket_path.parent() else {
|
||||
return Err(format!(
|
||||
"vm compute driver socket path '{}' has no parent directory",
|
||||
socket_path.display()
|
||||
));
|
||||
};
|
||||
let expected_uid = current_euid();
|
||||
prepare_private_socket_dir(parent, expected_uid)?;
|
||||
remove_stale_socket(socket_path, expected_uid)
|
||||
}
|
||||
|
||||
fn current_euid() -> u32 {
|
||||
rustix::process::geteuid().as_raw()
|
||||
}
|
||||
|
||||
fn prepare_private_socket_dir(
|
||||
socket_dir: &Path,
|
||||
expected_uid: u32,
|
||||
) -> std::result::Result<(), String> {
|
||||
std::fs::create_dir_all(socket_dir)
|
||||
.map_err(|err| format!("create socket dir {}: {err}", socket_dir.display()))?;
|
||||
let metadata = std::fs::symlink_metadata(socket_dir)
|
||||
.map_err(|err| format!("stat socket dir {}: {err}", socket_dir.display()))?;
|
||||
let file_type = metadata.file_type();
|
||||
if file_type.is_symlink() {
|
||||
return Err(format!(
|
||||
"socket dir {} is a symlink; refusing to use it",
|
||||
socket_dir.display()
|
||||
));
|
||||
}
|
||||
if !file_type.is_dir() {
|
||||
return Err(format!(
|
||||
"socket dir {} is not a directory",
|
||||
socket_dir.display()
|
||||
));
|
||||
}
|
||||
if metadata.uid() != expected_uid {
|
||||
return Err(format!(
|
||||
"socket dir {} is owned by uid {} but current euid is {}",
|
||||
socket_dir.display(),
|
||||
metadata.uid(),
|
||||
expected_uid
|
||||
));
|
||||
}
|
||||
std::fs::set_permissions(socket_dir, std::fs::Permissions::from_mode(0o700))
|
||||
.map_err(|err| format!("chmod socket dir {}: {err}", socket_dir.display()))
|
||||
}
|
||||
|
||||
fn remove_stale_socket(socket_path: &Path, expected_uid: u32) -> std::result::Result<(), String> {
|
||||
let metadata = match std::fs::symlink_metadata(socket_path) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
|
||||
Err(err) => return Err(format!("stat socket {}: {err}", socket_path.display())),
|
||||
};
|
||||
let file_type = metadata.file_type();
|
||||
if file_type.is_symlink() {
|
||||
return Err(format!(
|
||||
"socket {} is a symlink; refusing to remove it",
|
||||
socket_path.display()
|
||||
));
|
||||
}
|
||||
if metadata.uid() != expected_uid {
|
||||
return Err(format!(
|
||||
"socket {} is owned by uid {} but current euid is {}",
|
||||
socket_path.display(),
|
||||
metadata.uid(),
|
||||
expected_uid
|
||||
));
|
||||
}
|
||||
if !file_type.is_socket() {
|
||||
return Err(format!(
|
||||
"socket path {} exists but is not a Unix socket",
|
||||
socket_path.display()
|
||||
));
|
||||
}
|
||||
std::fs::remove_file(socket_path)
|
||||
.map_err(|err| format!("remove stale socket {}: {err}", socket_path.display()))
|
||||
}
|
||||
|
||||
fn restrict_socket_permissions(socket_path: &Path) -> std::result::Result<(), String> {
|
||||
std::fs::set_permissions(socket_path, std::fs::Permissions::from_mode(0o600))
|
||||
.map_err(|err| format!("chmod socket {}: {err}", socket_path.display()))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct PeerCredentials {
|
||||
uid: u32,
|
||||
pid: Option<i32>,
|
||||
}
|
||||
|
||||
fn peer_credentials(stream: &UnixStream) -> std::result::Result<PeerCredentials, String> {
|
||||
let credentials = stream
|
||||
.peer_cred()
|
||||
.map_err(|err| format!("read peer credentials: {err}"))?;
|
||||
Ok(PeerCredentials {
|
||||
uid: credentials.uid(),
|
||||
pid: credentials.pid(),
|
||||
})
|
||||
}
|
||||
|
||||
fn authorize_peer_credentials(
|
||||
peer: PeerCredentials,
|
||||
driver_uid: u32,
|
||||
gateway_pid: Option<u32>,
|
||||
) -> std::result::Result<(), String> {
|
||||
if peer.uid != driver_uid {
|
||||
return Err(format!(
|
||||
"peer uid {} does not match current euid {}",
|
||||
peer.uid, driver_uid
|
||||
));
|
||||
}
|
||||
let Some(gateway_pid) = gateway_pid else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(peer_process_id) = peer.pid.and_then(|pid| u32::try_from(pid).ok()) else {
|
||||
return Err(format!(
|
||||
"peer pid is unavailable; expected gateway pid {gateway_pid}"
|
||||
));
|
||||
};
|
||||
if peer_process_id != gateway_pid {
|
||||
return Err(format!(
|
||||
"peer pid {peer_process_id} does not match expected gateway pid {gateway_pid}"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct AuthenticatedUnixIncoming {
|
||||
listener: UnixListener,
|
||||
expected_uid: u32,
|
||||
expected_peer_pid: Option<u32>,
|
||||
}
|
||||
|
||||
impl AuthenticatedUnixIncoming {
|
||||
fn new(listener: UnixListener, expected_peer_pid: Option<u32>) -> Self {
|
||||
Self {
|
||||
listener,
|
||||
expected_uid: current_euid(),
|
||||
expected_peer_pid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for AuthenticatedUnixIncoming {
|
||||
type Item = io::Result<UnixStream>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
loop {
|
||||
match this.listener.poll_accept(cx) {
|
||||
Poll::Ready(Ok((stream, _addr))) => {
|
||||
let authorized = peer_credentials(&stream).and_then(|peer| {
|
||||
authorize_peer_credentials(peer, this.expected_uid, this.expected_peer_pid)
|
||||
});
|
||||
match authorized {
|
||||
Ok(()) => return Poll::Ready(Some(Ok(stream))),
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"rejected vm compute driver UDS client"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Poll::Ready(Err(err)) => return Poll::Ready(Some(Err(err))),
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_vm_launch_config(args: &Args) -> std::result::Result<VmLaunchConfig, String> {
|
||||
let root_disk = args
|
||||
.vm_root_disk
|
||||
.clone()
|
||||
.ok_or_else(|| "--vm-root-disk is required in internal VM mode".to_string())?;
|
||||
let overlay_disk = args
|
||||
.vm_overlay_disk
|
||||
.clone()
|
||||
.ok_or_else(|| "--vm-overlay-disk is required in internal VM mode".to_string())?;
|
||||
let image_disk = args.vm_image_disk.clone();
|
||||
let exec_path = args
|
||||
.vm_exec
|
||||
.clone()
|
||||
.ok_or_else(|| "--vm-exec is required in internal VM mode".to_string())?;
|
||||
let console_output = args
|
||||
.vm_console_output
|
||||
.clone()
|
||||
.ok_or_else(|| "--vm-console-output is required in internal VM mode".to_string())?;
|
||||
|
||||
let backend = match args.vm_backend.as_deref() {
|
||||
Some("qemu") => VmBackend::Qemu,
|
||||
Some("libkrun") | None => VmBackend::Libkrun,
|
||||
Some(other) => return Err(format!("unknown VM backend: {other}")),
|
||||
};
|
||||
|
||||
Ok(VmLaunchConfig {
|
||||
root_disk,
|
||||
overlay_disk,
|
||||
image_disk,
|
||||
kernel_image: args.vm_kernel_image.clone(),
|
||||
vcpus: args.vm_vcpus,
|
||||
mem_mib: args.vm_mem_mib,
|
||||
exec_path,
|
||||
args: Vec::new(),
|
||||
env: args.vm_env.clone(),
|
||||
workdir: args.vm_workdir.clone(),
|
||||
log_level: args.vm_krun_log_level,
|
||||
console_output,
|
||||
backend,
|
||||
gpu_bdf: args.vm_gpu_bdf.clone(),
|
||||
tap_device: args.vm_tap_device.clone(),
|
||||
guest_ip: args.vm_guest_ip.clone(),
|
||||
host_ip: args.vm_host_ip.clone(),
|
||||
vsock_cid: args.vm_vsock_cid,
|
||||
guest_mac: args.vm_guest_mac.clone(),
|
||||
gateway_port: args.vm_gateway_port,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn maybe_reexec_internal_vm_with_runtime_env() -> Result<()> {
|
||||
use std::os::unix::process::CommandExt as _;
|
||||
|
||||
const REEXEC_ENV: &str = "__OPENSHELL_DRIVER_VM_REEXEC";
|
||||
|
||||
if std::env::var_os(REEXEC_ENV).is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let runtime_dir = configured_runtime_dir().map_err(|err| miette::miette!("{err}"))?;
|
||||
let runtime_str = runtime_dir.to_string_lossy();
|
||||
let needs_reexec = std::env::var_os("DYLD_LIBRARY_PATH")
|
||||
.is_none_or(|value| !value.to_string_lossy().contains(runtime_str.as_ref()));
|
||||
if !needs_reexec {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut dyld_paths = vec![runtime_dir.clone()];
|
||||
if let Some(existing) = std::env::var_os("DYLD_LIBRARY_PATH") {
|
||||
dyld_paths.extend(std::env::split_paths(&existing));
|
||||
}
|
||||
let joined = std::env::join_paths(&dyld_paths)
|
||||
.map_err(|err| miette::miette!("join DYLD_LIBRARY_PATH: {err}"))?;
|
||||
let exe = std::env::current_exe().into_diagnostic()?;
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
|
||||
// Use execvp() so the current process is *replaced* by the re-exec'd
|
||||
// binary — no wrapper process sits between the compute driver and
|
||||
// the actually-running VM launcher. That avoids two problems:
|
||||
// 1. An extra process level that survives SIGKILL of the driver
|
||||
// (the wrapper was reparenting the re-exec'd child to init).
|
||||
// 2. Signal forwarding: with a wrapper, a SIGTERM to the wrapper
|
||||
// doesn't reach the child unless we hand-roll forwarding.
|
||||
// After exec, the child inherits our PID and our procguard arming.
|
||||
let err = std::process::Command::new(exe)
|
||||
.args(&args)
|
||||
.env("DYLD_LIBRARY_PATH", &joined)
|
||||
.env(VM_RUNTIME_DIR_ENV, runtime_dir)
|
||||
.env(REEXEC_ENV, "1")
|
||||
.exec();
|
||||
// `exec()` only returns on failure.
|
||||
Err(miette::miette!("failed to re-exec with runtime env: {err}"))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
// Signature must match the macOS variant which can fail.
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn maybe_reexec_internal_vm_with_runtime_env() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
Args, ComputeDriverListenMode, PeerCredentials, authorize_peer_credentials,
|
||||
compute_driver_listen_mode,
|
||||
};
|
||||
use clap::Parser;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn peer_authorization_accepts_matching_uid_and_pid() {
|
||||
authorize_peer_credentials(
|
||||
PeerCredentials {
|
||||
uid: 1000,
|
||||
pid: Some(42),
|
||||
},
|
||||
1000,
|
||||
Some(42),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_authorization_rejects_wrong_pid() {
|
||||
let err = authorize_peer_credentials(
|
||||
PeerCredentials {
|
||||
uid: 1000,
|
||||
pid: Some(7),
|
||||
},
|
||||
1000,
|
||||
Some(42),
|
||||
)
|
||||
.expect_err("wrong pid should be rejected");
|
||||
assert!(err.contains("does not match expected gateway pid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_authorization_rejects_wrong_uid() {
|
||||
let err = authorize_peer_credentials(
|
||||
PeerCredentials {
|
||||
uid: 1001,
|
||||
pid: Some(42),
|
||||
},
|
||||
1000,
|
||||
Some(42),
|
||||
)
|
||||
.expect_err("wrong uid should be rejected");
|
||||
assert!(err.contains("does not match current euid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_authorization_rejects_missing_pid_when_expected() {
|
||||
let err = authorize_peer_credentials(
|
||||
PeerCredentials {
|
||||
uid: 1000,
|
||||
pid: None,
|
||||
},
|
||||
1000,
|
||||
Some(42),
|
||||
)
|
||||
.expect_err("missing pid should be rejected");
|
||||
assert!(err.contains("peer pid is unavailable"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peer_authorization_accepts_matching_uid_without_expected_pid() {
|
||||
authorize_peer_credentials(
|
||||
PeerCredentials {
|
||||
uid: 1000,
|
||||
pid: None,
|
||||
},
|
||||
1000,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listen_mode_rejects_default_tcp() {
|
||||
let args = Args::parse_from(["openshell-driver-vm"]);
|
||||
let err = compute_driver_listen_mode(&args).expect_err("default TCP should be disabled");
|
||||
assert!(err.contains("--bind-socket is required"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_gateway_otlp_endpoint() {
|
||||
let args = Args::try_parse_from([
|
||||
"openshell-driver-vm",
|
||||
"--otlp-endpoint",
|
||||
"http://127.0.0.1:4317",
|
||||
]);
|
||||
assert!(
|
||||
args.is_ok(),
|
||||
"VM driver should accept the gateway OTLP endpoint"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listen_mode_rejects_bind_address_without_tcp_opt_in() {
|
||||
let args = Args::parse_from(["openshell-driver-vm", "--bind-address", "127.0.0.1:50061"]);
|
||||
let err =
|
||||
compute_driver_listen_mode(&args).expect_err("TCP bind should require explicit opt-in");
|
||||
assert!(err.contains("--allow-unauthenticated-tcp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listen_mode_requires_bind_address_with_tcp_opt_in() {
|
||||
let args = Args::parse_from(["openshell-driver-vm", "--allow-unauthenticated-tcp"]);
|
||||
let err =
|
||||
compute_driver_listen_mode(&args).expect_err("TCP opt-in should require an address");
|
||||
assert!(err.contains("--bind-address is required"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listen_mode_accepts_explicit_unauthenticated_tcp() {
|
||||
let args = Args::parse_from([
|
||||
"openshell-driver-vm",
|
||||
"--allow-unauthenticated-tcp",
|
||||
"--bind-address",
|
||||
"127.0.0.1:50061",
|
||||
]);
|
||||
assert_eq!(
|
||||
compute_driver_listen_mode(&args).unwrap(),
|
||||
ComputeDriverListenMode::Tcp("127.0.0.1:50061".parse().unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listen_mode_requires_expected_peer_pid_for_uds() {
|
||||
let args = Args::parse_from([
|
||||
"openshell-driver-vm",
|
||||
"--bind-socket",
|
||||
"/tmp/compute-driver.sock",
|
||||
]);
|
||||
let err = compute_driver_listen_mode(&args)
|
||||
.expect_err("UDS should require gateway peer pid by default");
|
||||
assert!(err.contains("--expected-peer-pid is required"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listen_mode_accepts_uds_with_expected_peer_pid() {
|
||||
let args = Args::parse_from([
|
||||
"openshell-driver-vm",
|
||||
"--bind-socket",
|
||||
"/tmp/compute-driver.sock",
|
||||
"--expected-peer-pid",
|
||||
"42",
|
||||
]);
|
||||
assert_eq!(
|
||||
compute_driver_listen_mode(&args).unwrap(),
|
||||
ComputeDriverListenMode::Unix {
|
||||
socket_path: PathBuf::from("/tmp/compute-driver.sock"),
|
||||
expected_peer_pid: Some(42),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listen_mode_accepts_explicit_same_uid_uds_dev_mode() {
|
||||
let args = Args::parse_from([
|
||||
"openshell-driver-vm",
|
||||
"--bind-socket",
|
||||
"/tmp/compute-driver.sock",
|
||||
"--allow-same-uid-peer",
|
||||
]);
|
||||
assert_eq!(
|
||||
compute_driver_listen_mode(&args).unwrap(),
|
||||
ComputeDriverListenMode::Unix {
|
||||
socket_path: PathBuf::from("/tmp/compute-driver.sock"),
|
||||
expected_peer_pid: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1207,11 +1207,15 @@ mod tests {
|
||||
|
||||
let expected = format!(
|
||||
"sqlite:{}",
|
||||
tmp.path().join("openshell/gateway/openshell.db").display()
|
||||
tmp.path()
|
||||
.join("openshell")
|
||||
.join("gateway")
|
||||
.join("openshell.db")
|
||||
.display()
|
||||
);
|
||||
assert!(local_tls.is_none());
|
||||
assert_eq!(args.db_url.as_deref(), Some(expected.as_str()));
|
||||
assert!(tmp.path().join("openshell/gateway").is_dir());
|
||||
assert!(tmp.path().join("openshell").join("gateway").is_dir());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -42,11 +42,21 @@ use openshell_core::proto::{
|
||||
SandboxTemplate, ServiceEndpoint, SshSession,
|
||||
};
|
||||
use openshell_core::{ObjectLabels, ObjectWorkspace};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use openshell_driver_docker::DockerComputeDriver;
|
||||
#[cfg(target_os = "windows")]
|
||||
use openshell_driver_kubernetes::KubernetesComputeConfig;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use openshell_driver_kubernetes::{
|
||||
ComputeDriverService as KubernetesDriverService, KubernetesComputeDriver,
|
||||
ComputeDriverService as KubernetesDriverService, KubernetesComputeConfig,
|
||||
KubernetesComputeDriver,
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
use openshell_driver_podman::PodmanComputeConfig;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use openshell_driver_podman::{
|
||||
ComputeDriverService as PodmanDriverService, PodmanComputeConfig, PodmanComputeDriver,
|
||||
};
|
||||
use openshell_driver_podman::{ComputeDriverService as PodmanDriverService, PodmanComputeDriver};
|
||||
use prost::Message;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
@@ -268,6 +278,7 @@ trait ShutdownCleanup: Send + Sync {
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
impl ShutdownCleanup for DockerComputeDriver {
|
||||
async fn cleanup_on_shutdown(&self) -> Result<(), String> {
|
||||
let stopped = self
|
||||
@@ -293,6 +304,7 @@ trait StartupResume: Send + Sync {
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
impl StartupResume for DockerComputeDriver {
|
||||
async fn resume_sandbox(&self, sandbox_id: &str, sandbox_name: &str) -> Result<bool, String> {
|
||||
Self::resume_sandbox(self, sandbox_id, sandbox_name)
|
||||
@@ -310,6 +322,11 @@ const ORPHAN_GRACE_PERIOD: Duration = Duration::from_secs(300);
|
||||
// Re-export the shared error type under the name used by this module.
|
||||
pub use openshell_core::ComputeDriverError as ComputeError;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn unsupported_compute_driver_error(driver: &str) -> ComputeError {
|
||||
ComputeError::Message(format!("{driver} compute driver is unsupported on Windows"))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ManagedDriverProcess {
|
||||
child: std::sync::Mutex<Option<tokio::process::Child>>,
|
||||
@@ -317,6 +334,7 @@ pub struct ManagedDriverProcess {
|
||||
}
|
||||
|
||||
impl ManagedDriverProcess {
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn new(child: tokio::process::Child, socket_path: PathBuf) -> Self {
|
||||
Self {
|
||||
child: std::sync::Mutex::new(Some(child)),
|
||||
@@ -710,6 +728,7 @@ impl ComputeRuntime {
|
||||
self.delete_gates.entry_count()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub async fn new_docker(
|
||||
config: openshell_core::Config,
|
||||
docker_config: DockerComputeConfig,
|
||||
@@ -742,6 +761,20 @@ impl ComputeRuntime {
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub async fn new_docker(
|
||||
_config: openshell_core::Config,
|
||||
_docker_config: DockerComputeConfig,
|
||||
_store: Arc<Store>,
|
||||
_sandbox_index: SandboxIndex,
|
||||
_sandbox_watch_bus: SandboxWatchBus,
|
||||
_tracing_log_bus: TracingLogBus,
|
||||
_supervisor_sessions: Arc<SupervisorSessionRegistry>,
|
||||
) -> Result<Self, ComputeError> {
|
||||
Err(unsupported_compute_driver_error("Docker"))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub async fn new_kubernetes(
|
||||
config: KubernetesComputeConfig,
|
||||
store: Arc<Store>,
|
||||
@@ -769,6 +802,18 @@ impl ComputeRuntime {
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub async fn new_kubernetes(
|
||||
_config: KubernetesComputeConfig,
|
||||
_store: Arc<Store>,
|
||||
_sandbox_index: SandboxIndex,
|
||||
_sandbox_watch_bus: SandboxWatchBus,
|
||||
_tracing_log_bus: TracingLogBus,
|
||||
_supervisor_sessions: Arc<SupervisorSessionRegistry>,
|
||||
) -> Result<Self, ComputeError> {
|
||||
Err(unsupported_compute_driver_error("Kubernetes"))
|
||||
}
|
||||
|
||||
pub(crate) async fn new_remote_driver(
|
||||
endpoint: AcquiredRemoteDriverEndpoint,
|
||||
store: Arc<Store>,
|
||||
@@ -793,6 +838,7 @@ impl ComputeRuntime {
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub async fn new_podman(
|
||||
config: PodmanComputeConfig,
|
||||
store: Arc<Store>,
|
||||
@@ -820,6 +866,18 @@ impl ComputeRuntime {
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub async fn new_podman(
|
||||
_config: PodmanComputeConfig,
|
||||
_store: Arc<Store>,
|
||||
_sandbox_index: SandboxIndex,
|
||||
_sandbox_watch_bus: SandboxWatchBus,
|
||||
_tracing_log_bus: TracingLogBus,
|
||||
_supervisor_sessions: Arc<SupervisorSessionRegistry>,
|
||||
) -> Result<Self, ComputeError> {
|
||||
Err(unsupported_compute_driver_error("Podman"))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn default_image(&self) -> &str {
|
||||
&self.default_image
|
||||
@@ -7246,4 +7304,16 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(stored.object_workspace(), "alpha");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn windows_compute_driver_stubs_report_unsupported() {
|
||||
for driver in ["Docker", "Kubernetes", "Podman"] {
|
||||
let message = unsupported_compute_driver_error(driver).to_string();
|
||||
assert!(
|
||||
message.contains("unsupported on Windows"),
|
||||
"{driver} stub message should be explicit, got: {message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,11 @@ use tonic::transport::Endpoint;
|
||||
#[cfg(unix)]
|
||||
use tower::service_fn;
|
||||
|
||||
#[cfg(unix)]
|
||||
const DRIVER_BIN_NAME: &str = "openshell-driver-vm";
|
||||
#[cfg(unix)]
|
||||
const COMPUTE_DRIVER_SOCKET_RUN_DIR: &str = "run";
|
||||
#[cfg(unix)]
|
||||
const COMPUTE_DRIVER_SOCKET_NAME: &str = "compute-driver.sock";
|
||||
|
||||
/// Configuration for launching and talking to the VM compute driver.
|
||||
@@ -139,6 +142,7 @@ impl VmComputeConfig {
|
||||
4096
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[must_use]
|
||||
fn default_driver_search_dirs(home: Option<PathBuf>) -> Vec<PathBuf> {
|
||||
let mut dirs = Vec::new();
|
||||
@@ -189,6 +193,7 @@ pub struct VmGuestTlsPaths {
|
||||
/// `/usr/local/libexec/openshell`, `/usr/local/libexec`.
|
||||
/// 3. Sibling of the gateway's own executable (last-resort fallback so
|
||||
/// local development builds still work out of the box).
|
||||
#[cfg(unix)]
|
||||
pub fn resolve_compute_driver_bin(vm_config: &VmComputeConfig) -> Result<PathBuf> {
|
||||
let mut searched: Vec<PathBuf> = Vec::new();
|
||||
|
||||
@@ -227,6 +232,7 @@ pub fn resolve_compute_driver_bin(vm_config: &VmComputeConfig) -> Result<PathBuf
|
||||
)))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn resolve_driver_search_dirs(vm_config: &VmComputeConfig) -> Vec<PathBuf> {
|
||||
vm_config.driver_dir.clone().map_or_else(
|
||||
|| {
|
||||
@@ -248,6 +254,7 @@ fn resolve_driver_search_dirs(vm_config: &VmComputeConfig) -> Vec<PathBuf> {
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn push_unique_path(paths: &mut Vec<PathBuf>, path: PathBuf) {
|
||||
if !paths.iter().any(|existing| existing == &path) {
|
||||
paths.push(path);
|
||||
@@ -255,6 +262,7 @@ fn push_unique_path(paths: &mut Vec<PathBuf>, path: PathBuf) {
|
||||
}
|
||||
|
||||
/// Path of the Unix domain socket the driver will listen on.
|
||||
#[cfg(unix)]
|
||||
pub fn compute_driver_socket_path(vm_config: &VmComputeConfig) -> PathBuf {
|
||||
vm_config
|
||||
.state_dir
|
||||
@@ -527,7 +535,21 @@ fn append_otlp_args(command: &mut Command, otlp_config: Option<&OtlpConfig>) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn unsupported_vm_message() -> &'static str {
|
||||
"VM compute driver is unsupported on Windows"
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub async fn spawn(
|
||||
_config: &Config,
|
||||
_vm_config: &VmComputeConfig,
|
||||
_otlp_config: Option<&OtlpConfig>,
|
||||
) -> Result<AcquiredRemoteDriverEndpoint> {
|
||||
Err(Error::config(unsupported_vm_message()))
|
||||
}
|
||||
|
||||
#[cfg(all(not(unix), not(target_os = "windows")))]
|
||||
pub async fn spawn(
|
||||
_config: &Config,
|
||||
_vm_config: &VmComputeConfig,
|
||||
@@ -947,3 +969,11 @@ mod tests {
|
||||
assert!(!socket_path.exists());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, target_os = "windows"))]
|
||||
mod windows_tests {
|
||||
#[test]
|
||||
fn windows_spawn_reports_unsupported() {
|
||||
assert!(super::unsupported_vm_message().contains("unsupported on Windows"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user