Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions crates/openshell-core/src/driver_mounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@

use std::path::Path;

/// `SELinux` relabelling mode for bind mounts.
///
/// On hosts with `SELinux` enabled (e.g. Fedora, RHEL) a bind-mounted path
/// must be relabelled so the container process can access it.
///
/// * `shared` (`:z`) — the label is shared across all containers that mount
/// the same path. Safe when multiple sandboxes read the same data set.
/// * `private` (`:Z`) — the label is private to *this* container. The host
/// directory becomes inaccessible to other containers (and potentially to
/// the host) until the container is removed. Use only when exclusive
/// ownership is acceptable.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SelinuxLabel {
/// Shared `SELinux` label (`:z`).
Shared,
/// Private `SELinux` label (`:Z`).
Private,
}

const RESERVED_MOUNT_TARGETS: &[&str] = &[
"/opt/openshell",
"/etc/openshell",
Expand Down
106 changes: 88 additions & 18 deletions crates/openshell-driver-docker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,8 @@ impl DockerSandboxDriverConfig {
}
}

use openshell_core::driver_mounts::SelinuxLabel;

#[derive(Debug, Clone, serde::Deserialize)]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
enum DockerDriverMountConfig {
Expand All @@ -311,6 +313,8 @@ enum DockerDriverMountConfig {
target: String,
#[serde(default = "default_true")]
read_only: bool,
#[serde(default)]
selinux_label: Option<SelinuxLabel>,
},
Volume {
source: String,
Expand Down Expand Up @@ -1761,29 +1765,90 @@ fn docker_driver_config(
Ok(config)
}

/// Collect user-supplied bind mounts as string-format binds.
///
/// Bind mounts use the legacy `Binds` field (`-v` syntax) rather than the
/// structured `Mount` API because the Docker Engine Mount object does not
/// support `SELinux` relabelling (`:z` / `:Z`). The string format does.
fn docker_driver_bind_strings(config: &DockerSandboxDriverConfig) -> Result<Vec<String>, Status> {
config
.mounts
.iter()
.filter_map(|m| match m {
DockerDriverMountConfig::Bind {
source,
target,
read_only,
selinux_label,
} => Some(docker_bind_string(
source,
target,
*read_only,
*selinux_label,
)),
_ => None,
})
.collect()
}

fn docker_bind_string(
source: &str,
target: &str,
read_only: bool,
selinux_label: Option<SelinuxLabel>,
) -> Result<String, Status> {
driver_mounts::validate_absolute_mount_source(source, "bind source")
.map_err(Status::failed_precondition)?;
// Legacy `-v` binds silently create missing source directories as empty,
// root-owned paths. The structured `--mount` API that was used before this
// change rejected missing sources at container-create time. Preserve that
// fail-fast behaviour with an explicit existence check.
if !Path::new(source).exists() {
return Err(Status::failed_precondition(format!(
"bind source path does not exist: {source}"
)));
}
driver_mounts::validate_container_mount_target(target).map_err(Status::failed_precondition)?;
let normalized_target = driver_mounts::normalize_mount_target(target);

let mut opts = Vec::new();
if read_only {
opts.push("ro");
}
match selinux_label {
Some(SelinuxLabel::Shared) => opts.push("z"),
Some(SelinuxLabel::Private) => opts.push("Z"),
None => {}
}

if opts.is_empty() {
Ok(format!("{source}:{normalized_target}"))
} else {
Ok(format!("{source}:{normalized_target}:{}", opts.join(",")))
}
}

/// Collect user-supplied non-bind mounts as structured `Mount` objects.
fn docker_driver_mounts(config: &DockerSandboxDriverConfig) -> Result<Vec<Mount>, Status> {
config.mounts.iter().map(docker_mount_from_config).collect()
config
.mounts
.iter()
.filter_map(|m| docker_mount_from_config(m).transpose())
.collect()
}

fn docker_mount_from_config(config: &DockerDriverMountConfig) -> Result<Mount, Status> {
fn docker_mount_from_config(config: &DockerDriverMountConfig) -> Result<Option<Mount>, Status> {
match config {
DockerDriverMountConfig::Bind {
source,
target,
read_only,
} => Ok(Mount {
typ: Some(MountTypeEnum::BIND),
source: Some(source.clone()),
target: Some(target.clone()),
read_only: Some(*read_only),
..Default::default()
}),
DockerDriverMountConfig::Bind { .. } => {
// Bind mounts are handled via docker_driver_bind_strings.
Ok(None)
}
DockerDriverMountConfig::Volume {
source,
target,
read_only,
subpath,
} => Ok(Mount {
} => Ok(Some(Mount {
typ: Some(MountTypeEnum::VOLUME),
source: Some(source.clone()),
target: Some(target.clone()),
Expand All @@ -1793,13 +1858,13 @@ fn docker_mount_from_config(config: &DockerDriverMountConfig) -> Result<Mount, S
..Default::default()
}),
..Default::default()
}),
})),
DockerDriverMountConfig::Tmpfs {
target,
options,
size_bytes,
mode,
} => Ok(Mount {
} => Ok(Some(Mount {
typ: Some(MountTypeEnum::TMPFS),
target: Some(target.clone()),
tmpfs_options: Some(MountTmpfsOptions {
Expand All @@ -1818,7 +1883,7 @@ fn docker_mount_from_config(config: &DockerDriverMountConfig) -> Result<Mount, S
.transpose()?,
}),
..Default::default()
}),
})),
DockerDriverMountConfig::Image { .. } => Err(Status::failed_precondition(
"invalid docker driver_config: docker image mounts are not supported",
)),
Expand Down Expand Up @@ -2261,6 +2326,7 @@ fn build_container_create_body_with_gpu_devices(
.ok_or_else(|| Status::invalid_argument("sandbox.spec.template is required"))?;
let resource_limits = docker_resource_limits(template)?;
let user_mounts = docker_driver_mounts(driver_config)?;
let user_bind_strings = docker_driver_bind_strings(driver_config)?;
let device_requests = gpu_device_ids.map(|device_ids| {
vec![DeviceRequest {
driver: Some("cdi".to_string()),
Expand Down Expand Up @@ -2298,7 +2364,11 @@ fn build_container_create_body_with_gpu_devices(
memory: resource_limits.memory_bytes,
pids_limit: docker_pids_limit(config.sandbox_pids_limit)?,
device_requests,
binds: Some(build_binds(sandbox, config)?),
binds: {
let mut binds = build_binds(sandbox, config)?;
binds.extend(user_bind_strings);
Some(binds)
},
mounts: Some(user_mounts),
restart_policy: Some(RestartPolicy {
name: Some(RestartPolicyNameEnum::UNLESS_STOPPED),
Expand Down
Loading
Loading