@@ -241,6 +241,16 @@ pub struct KubernetesComputeConfig {
241241 deserialize_with = "deserialize_provider_spiffe_workload_api_socket_path"
242242 ) ]
243243 pub provider_spiffe_workload_api_socket_path : String ,
244+ /// UID used for the supervisor container `securityContext.runAsUser` and
245+ /// PVC init container ownership operations. When empty, the driver
246+ /// auto-detects from OpenShift SCC annotations on the target namespace;
247+ /// if those are also absent, falls back to `1000`.
248+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
249+ pub sandbox_uid : Option < u32 > ,
250+ /// GID used alongside `sandbox_uid` for PVC init container operations.
251+ /// When empty and `sandbox_uid` is set, defaults to the resolved UID.
252+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
253+ pub sandbox_gid : Option < u32 > ,
244254}
245255
246256/// Lower bound enforced by kubelet for projected SA tokens.
@@ -251,6 +261,18 @@ pub const MIN_SA_TOKEN_TTL_SECS: i64 = 600;
251261/// pod start).
252262pub const MAX_SA_TOKEN_TTL_SECS : i64 = 86_400 ;
253263
264+ /// Default sandbox UID used when neither config nor OpenShift SCC annotations
265+ /// provide a resolved value.
266+ pub ( crate ) const DEFAULT_SANDBOX_UID : u32 = 1000 ;
267+
268+ /// The annotation key for the OpenShift ServiceAccount UID range.
269+ /// Format: `<start>/<size>` (e.g. `1000000000/10000`).
270+ pub const ANNOTATION_SCC_UID_RANGE : & str = "openshift.io/sa.scc.uid-range" ;
271+
272+ /// The annotation key for the OpenShift ServiceAccount supplemental groups.
273+ /// Format: `<start>/<size>` (e.g. `1000000000/10000`).
274+ pub const ANNOTATION_SCC_SUPPLEMENTAL_GROUPS : & str = "openshift.io/sa.scc.supplemental-groups" ;
275+
254276impl Default for KubernetesComputeConfig {
255277 fn default ( ) -> Self {
256278 Self {
@@ -277,6 +299,8 @@ impl Default for KubernetesComputeConfig {
277299 default_runtime_class_name : String :: new ( ) ,
278300 sa_token_ttl_secs : 3600 ,
279301 provider_spiffe_workload_api_socket_path : String :: new ( ) ,
302+ sandbox_uid : None ,
303+ sandbox_gid : None ,
280304 }
281305 }
282306}
@@ -308,6 +332,64 @@ impl KubernetesComputeConfig {
308332 & self . provider_spiffe_workload_api_socket_path ,
309333 )
310334 }
335+
336+ /// Resolve the sandbox UID/GID pair.
337+ ///
338+ /// Resolution order:
339+ /// 1. Configured `sandbox_uid` / `sandbox_gid` (explicit override)
340+ /// 2. OpenShift SCC namespace annotations (`sa.scc.uid-range`,
341+ /// `sa.scc.supplemental-groups`) — passed in as the optional
342+ /// `namespace_annotations` map
343+ /// 3. Fallback defaults: UID=`1000`, GID=UID
344+ pub fn resolve_sandbox_uid (
345+ & self ,
346+ namespace_annotations : Option < & std:: collections:: BTreeMap < String , String > > ,
347+ ) -> u32 {
348+ if let Some ( uid) = self . sandbox_uid {
349+ return uid;
350+ }
351+ // Try OpenShift SCC annotation.
352+ if let Some ( anns) = namespace_annotations {
353+ if let Some ( range) = anns. get ( ANNOTATION_SCC_UID_RANGE ) {
354+ if let Some ( uid) = Self :: from_open_shift_uid_range ( range) {
355+ return uid;
356+ }
357+ }
358+ }
359+ DEFAULT_SANDBOX_UID
360+ }
361+
362+ pub fn resolve_sandbox_gid (
363+ & self ,
364+ resolved_uid : u32 ,
365+ _namespace_annotations : Option < & std:: collections:: BTreeMap < String , String > > ,
366+ ) -> u32 {
367+ self . sandbox_gid
368+ . or_else ( || self . sandbox_uid )
369+ . unwrap_or ( resolved_uid)
370+ }
371+
372+ /// Parse OpenShift SCC `sa.scc.uid-range` annotation.
373+ ///
374+ /// Format: `<start>/<size>` (e.g. `1000000000/10000`).
375+ pub fn from_open_shift_uid_range ( annotation : & str ) -> Option < u32 > {
376+ let ( start, _) = annotation. split_once ( '/' ) ?;
377+ start
378+ . trim ( )
379+ . parse :: < u32 > ( )
380+ . ok ( )
381+ . filter ( |& uid| uid >= openshell_policy:: MIN_SANDBOX_UID )
382+ }
383+
384+ /// Parse OpenShift SCC `sa.scc.supplemental-groups` annotation.
385+ pub fn from_open_shift_supplemental_groups ( annotation : & str ) -> Option < u32 > {
386+ let ( start, _) = annotation. split_once ( '/' ) ?;
387+ start
388+ . trim ( )
389+ . parse :: < u32 > ( )
390+ . ok ( )
391+ . filter ( |& gid| gid >= openshell_policy:: MIN_SANDBOX_UID )
392+ }
311393}
312394
313395fn validate_provider_spiffe_workload_api_socket_path_value (
@@ -345,6 +427,7 @@ fn validate_provider_spiffe_workload_api_socket_path_value(
345427#[ cfg( test) ]
346428mod tests {
347429 use super :: * ;
430+ use std:: collections:: BTreeMap as HashMap ;
348431
349432 #[ test]
350433 fn default_workspace_storage_size_is_2gi ( ) {
@@ -515,4 +598,128 @@ mod tests {
515598 let cfg: KubernetesComputeConfig = serde_json:: from_value ( json) . unwrap ( ) ;
516599 assert_eq ! ( cfg. image_pull_secrets, [ "regcred" , "backup-regcred" ] ) ;
517600 }
601+
602+ #[ test]
603+ fn default_sandbox_uid_and_gid_are_none ( ) {
604+ let cfg = KubernetesComputeConfig :: default ( ) ;
605+ assert_eq ! ( cfg. sandbox_uid, None ) ;
606+ assert_eq ! ( cfg. sandbox_gid, None ) ;
607+ }
608+
609+ #[ test]
610+ fn serde_override_sandbox_uid ( ) {
611+ let json = serde_json:: json!( {
612+ "sandbox_uid" : 1500
613+ } ) ;
614+ let cfg: KubernetesComputeConfig = serde_json:: from_value ( json) . unwrap ( ) ;
615+ assert_eq ! ( cfg. sandbox_uid, Some ( 1500 ) ) ;
616+ }
617+
618+ #[ test]
619+ fn serde_override_sandbox_gid ( ) {
620+ let json = serde_json:: json!( {
621+ "sandbox_gid" : 2000
622+ } ) ;
623+ let cfg: KubernetesComputeConfig = serde_json:: from_value ( json) . unwrap ( ) ;
624+ assert_eq ! ( cfg. sandbox_gid, Some ( 2000 ) ) ;
625+ }
626+
627+ #[ test]
628+ fn parse_openshift_uid_range ( ) {
629+ assert_eq ! (
630+ KubernetesComputeConfig :: from_open_shift_uid_range( "1000000000/10000" ) ,
631+ Some ( 1000000000 )
632+ ) ;
633+ assert_eq ! (
634+ KubernetesComputeConfig :: from_open_shift_uid_range( "1000/50000" ) ,
635+ Some ( 1000 )
636+ ) ;
637+ }
638+
639+ #[ test]
640+ fn parse_openshift_uid_range_rejects_below_min ( ) {
641+ // 999 is below MIN_SANDBOX_UID (1000) — should be rejected.
642+ assert_eq ! (
643+ KubernetesComputeConfig :: from_open_shift_uid_range( "999/50000" ) ,
644+ None
645+ ) ;
646+ }
647+
648+ #[ test]
649+ fn parse_openshift_supplemental_groups ( ) {
650+ assert_eq ! (
651+ KubernetesComputeConfig :: from_open_shift_supplemental_groups( "1000/50000" ) ,
652+ Some ( 1000 )
653+ ) ;
654+ }
655+
656+ #[ test]
657+ fn resolve_sandbox_uid_prefers_config ( ) {
658+ let cfg = KubernetesComputeConfig {
659+ sandbox_uid : Some ( 5000 ) ,
660+ ..KubernetesComputeConfig :: default ( )
661+ } ;
662+ // Config value should win even when annotations are present.
663+ let mut anns: HashMap < String , String > = HashMap :: new ( ) ;
664+ anns. insert (
665+ ANNOTATION_SCC_UID_RANGE . to_string ( ) ,
666+ "1000000000/10000" . to_string ( ) ,
667+ ) ;
668+ assert_eq ! ( cfg. resolve_sandbox_uid( Some ( & anns) ) , 5000 ) ;
669+ }
670+
671+ #[ test]
672+ fn resolve_sandbox_uid_falls_back_to_openshift_annotation ( ) {
673+ let cfg = KubernetesComputeConfig :: default ( ) ;
674+ let mut anns: HashMap < String , String > = HashMap :: new ( ) ;
675+ anns. insert (
676+ ANNOTATION_SCC_UID_RANGE . to_string ( ) ,
677+ "1000000000/10000" . to_string ( ) ,
678+ ) ;
679+ assert_eq ! ( cfg. resolve_sandbox_uid( Some ( & anns) ) , 1000000000 ) ;
680+ }
681+
682+ #[ test]
683+ fn resolve_sandbox_uid_falls_back_to_default ( ) {
684+ let cfg = KubernetesComputeConfig :: default ( ) ;
685+ // No config, no annotations.
686+ assert_eq ! ( cfg. resolve_sandbox_uid( None ) , DEFAULT_SANDBOX_UID ) ;
687+ // Empty annotations map.
688+ let anns: HashMap < String , String > = HashMap :: new ( ) ;
689+ assert_eq ! ( cfg. resolve_sandbox_uid( Some ( & anns) ) , DEFAULT_SANDBOX_UID ) ;
690+ }
691+
692+ #[ test]
693+ fn resolve_sandbox_gid_prefers_config ( ) {
694+ let cfg = KubernetesComputeConfig {
695+ sandbox_uid : Some ( 5000 ) ,
696+ sandbox_gid : Some ( 6000 ) ,
697+ ..KubernetesComputeConfig :: default ( )
698+ } ;
699+ assert_eq ! (
700+ cfg. resolve_sandbox_gid( cfg. resolve_sandbox_uid( None ) , None ) ,
701+ 6000
702+ ) ;
703+ }
704+
705+ #[ test]
706+ fn resolve_sandbox_gid_falls_back_to_uid ( ) {
707+ let cfg = KubernetesComputeConfig {
708+ sandbox_uid : Some ( 5000 ) ,
709+ ..KubernetesComputeConfig :: default ( )
710+ } ;
711+ // sandbox_gid is None, should fall back to sandbox_uid.
712+ assert_eq ! (
713+ cfg. resolve_sandbox_gid( cfg. resolve_sandbox_uid( None ) , None ) ,
714+ 5000
715+ ) ;
716+ }
717+
718+ #[ test]
719+ fn resolve_sandbox_gid_falls_back_to_resolved_uid ( ) {
720+ let cfg = KubernetesComputeConfig :: default ( ) ;
721+ // Both are None, should use the resolved UID.
722+ let uid = cfg. resolve_sandbox_uid ( None ) ;
723+ assert_eq ! ( cfg. resolve_sandbox_gid( uid, None ) , uid) ;
724+ }
518725}
0 commit comments