Skip to content

Commit 3e86e52

Browse files
committed
fix(kubernetes): address PVC subPath review feedback
Signed-off-by: mjamiv <michael.commack@gmail.com>
1 parent d97ac13 commit 3e86e52

7 files changed

Lines changed: 359 additions & 131 deletions

File tree

crates/openshell-core/src/driver_mounts.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
//! Shared validation helpers for driver-config mounts.
55
6+
use std::collections::HashSet;
67
use std::path::Path;
78

89
const RESERVED_MOUNT_TARGETS: &[&str] = &[
@@ -12,6 +13,11 @@ const RESERVED_MOUNT_TARGETS: &[&str] = &[
1213
"/run/netns",
1314
];
1415

16+
/// Serde default helper for mount options that default to read-only.
17+
pub fn default_true() -> bool {
18+
true
19+
}
20+
1521
/// Validate a non-empty driver mount source.
1622
pub fn validate_mount_source(source: &str, field: &str) -> Result<String, String> {
1723
let source = source.trim();
@@ -96,13 +102,30 @@ fn normalize_container_mount_target(target: &str) -> String {
96102
target.trim_end_matches('/').to_string()
97103
}
98104

99-
fn path_is_or_under(path: &str, parent: &str) -> bool {
105+
/// Return true when `path` is exactly `parent` or is contained below it.
106+
pub fn path_is_or_under(path: &str, parent: &str) -> bool {
100107
path == parent
101108
|| path
102109
.strip_prefix(parent)
103110
.is_some_and(|rest| rest.starts_with('/'))
104111
}
105112

113+
/// Validate that already-normalized driver mount targets are unique.
114+
pub fn validate_unique_mount_targets<'a>(
115+
targets: impl IntoIterator<Item = &'a str>,
116+
driver_name: &str,
117+
) -> Result<(), String> {
118+
let mut seen = HashSet::new();
119+
for target in targets {
120+
if !seen.insert(target) {
121+
return Err(format!(
122+
"duplicate {driver_name} driver_config mount target '{target}'"
123+
));
124+
}
125+
}
126+
Ok(())
127+
}
128+
106129
#[cfg(test)]
107130
mod tests {
108131
use super::*;
@@ -144,6 +167,24 @@ mod tests {
144167
);
145168
}
146169

170+
#[test]
171+
fn path_is_or_under_matches_boundaries() {
172+
assert!(path_is_or_under("/sandbox", "/sandbox"));
173+
assert!(path_is_or_under("/sandbox/work", "/sandbox"));
174+
assert!(!path_is_or_under("/sandbox-work", "/sandbox"));
175+
}
176+
177+
#[test]
178+
fn unique_mount_targets_rejects_duplicates() {
179+
let err =
180+
validate_unique_mount_targets(["/sandbox/work", "/sandbox/work"], "test").unwrap_err();
181+
182+
assert_eq!(
183+
err,
184+
"duplicate test driver_config mount target '/sandbox/work'"
185+
);
186+
}
187+
147188
#[test]
148189
fn mount_subpath_must_be_relative_without_parent_dirs() {
149190
assert_eq!(validate_mount_subpath(" project/a ").unwrap(), "project/a");

crates/openshell-driver-docker/src/lib.rs

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ use openshell_core::proto_struct::{
4949
deserialize_optional_non_empty_string_list, struct_to_json_value,
5050
};
5151
use openshell_core::{Config, Error, Result as CoreResult};
52-
use std::collections::{HashMap, HashSet};
52+
use std::collections::HashMap;
5353
use std::io::Read;
5454
use std::net::{IpAddr, SocketAddr};
5555
use std::path::{Path, PathBuf};
@@ -309,13 +309,13 @@ enum DockerDriverMountConfig {
309309
Bind {
310310
source: String,
311311
target: String,
312-
#[serde(default = "default_true")]
312+
#[serde(default = "driver_mounts::default_true")]
313313
read_only: bool,
314314
},
315315
Volume {
316316
source: String,
317317
target: String,
318-
#[serde(default = "default_true")]
318+
#[serde(default = "driver_mounts::default_true")]
319319
read_only: bool,
320320
#[serde(default)]
321321
subpath: Option<String>,
@@ -332,17 +332,13 @@ enum DockerDriverMountConfig {
332332
Image {
333333
source: String,
334334
target: String,
335-
#[serde(default = "default_true")]
335+
#[serde(default = "driver_mounts::default_true")]
336336
read_only: bool,
337337
#[serde(default)]
338338
subpath: Option<String>,
339339
},
340340
}
341341

342-
fn default_true() -> bool {
343-
true
344-
}
345-
346342
type WatchStream =
347343
Pin<Box<dyn Stream<Item = Result<WatchSandboxesEvent, Status>> + Send + 'static>>;
348344

@@ -1854,7 +1850,7 @@ fn validate_docker_driver_mounts(
18541850
mounts: &[DockerDriverMountConfig],
18551851
enable_bind_mounts: bool,
18561852
) -> Result<(), Status> {
1857-
let mut targets = HashSet::new();
1853+
let mut targets = Vec::with_capacity(mounts.len());
18581854
for mount in mounts {
18591855
let target = match mount {
18601856
DockerDriverMountConfig::Bind { source, target, .. } => {
@@ -1908,13 +1904,10 @@ fn validate_docker_driver_mounts(
19081904
};
19091905
let target = driver_mounts::validate_container_mount_target(target)
19101906
.map_err(Status::failed_precondition)?;
1911-
if !targets.insert(target.clone()) {
1912-
return Err(Status::failed_precondition(format!(
1913-
"duplicate docker driver_config mount target '{target}'"
1914-
)));
1915-
}
1907+
targets.push(target);
19161908
}
1917-
Ok(())
1909+
driver_mounts::validate_unique_mount_targets(targets.iter().map(String::as_str), "docker")
1910+
.map_err(Status::failed_precondition)
19181911
}
19191912

19201913
fn validate_optional_positive_integral_i64(

crates/openshell-driver-docker/src/tests.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,39 @@ fn driver_config_allows_explicit_writable_volume_mounts() {
755755
assert_eq!(mounts[0].read_only, Some(false));
756756
}
757757

758+
#[test]
759+
fn driver_config_rejects_duplicate_mount_targets() {
760+
let mut sandbox = test_sandbox();
761+
sandbox
762+
.spec
763+
.as_mut()
764+
.unwrap()
765+
.template
766+
.as_mut()
767+
.unwrap()
768+
.driver_config = Some(json_struct(serde_json::json!({
769+
"mounts": [
770+
{
771+
"type": "volume",
772+
"source": "work-nfs",
773+
"target": "/sandbox/work"
774+
},
775+
{
776+
"type": "tmpfs",
777+
"target": "/sandbox/work"
778+
}
779+
]
780+
})));
781+
782+
let err = build_container_create_body(&sandbox, &runtime_config()).unwrap_err();
783+
784+
assert_eq!(err.code(), tonic::Code::FailedPrecondition);
785+
assert!(
786+
err.message()
787+
.contains("duplicate docker driver_config mount target")
788+
);
789+
}
790+
758791
#[test]
759792
fn driver_config_rejects_bind_mounts_unless_enabled() {
760793
let mut sandbox = test_sandbox();

crates/openshell-driver-kubernetes/README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,16 @@ resource details.
114114

115115
Use PVC volumes to mount existing Kubernetes PersistentVolumeClaims into the
116116
agent container. PVC volumes and mounts default to read-only unless
117-
`read_only: false` is set explicitly. A read-only PVC volume cannot be mounted
118-
read-write. The driver rejects duplicate volume names, mounts that reference
119-
unknown volumes, non-normalized or protected mount paths, and absolute or
120-
parent-traversing `sub_path` values.
121-
122-
Any explicit driver-config mount under `/sandbox/` disables the driver's
123-
default `/sandbox` workspace PVC injection for that sandbox. This keeps image
124-
contents fresh while allowing selected durable data paths to come from an
125-
external PVC.
117+
`read_only: false` is set explicitly. Read-write access requires
118+
`read_only: false` on both the PVC volume and each writable mount. The driver
119+
rejects duplicate volume names, invalid DNS-1123 volume or PVC claim names,
120+
mounts that reference unknown volumes, non-normalized or protected mount paths,
121+
and absolute or parent-traversing `sub_path` values.
122+
123+
Any explicit driver-config mount under `/sandbox` disables the driver's
124+
default `/sandbox` workspace PVC injection for that sandbox. Only the explicit
125+
mount paths persist through the external PVC; other `/sandbox` paths come from
126+
the current sandbox image.
126127

127128
```shell
128129
openshell sandbox create \

0 commit comments

Comments
 (0)