@@ -207,12 +207,28 @@ func BuildPod(notebook *marimov1alpha1.MarimoNotebook) *corev1.Pod {
207207 // Check if any sidecar uses FUSE (privileged) - if so, marimo container needs
208208 // HostToContainer propagation
209209 hasFUSESidecar := false
210+ hasSSHFSSidecar := false
210211 for _ , sidecar := range allSidecars {
211212 if sidecar .SecurityContext != nil && sidecar .SecurityContext .Privileged != nil &&
212213 * sidecar .SecurityContext .Privileged {
213214 hasFUSESidecar = true
214- break
215215 }
216+ if strings .HasPrefix (sidecar .Name , "sshfs-" ) {
217+ hasSSHFSSidecar = true
218+ }
219+ }
220+
221+ // Add ssh-pubkey secret volume if any sshfs sidecar exists
222+ // The plugin creates this secret from the user's public key
223+ if hasSSHFSSidecar {
224+ volumes = append (volumes , corev1.Volume {
225+ Name : "ssh-pubkey" ,
226+ VolumeSource : corev1.VolumeSource {
227+ Secret : & corev1.SecretVolumeSource {
228+ SecretName : "ssh-pubkey" ,
229+ },
230+ },
231+ })
216232 }
217233
218234 // If there are FUSE sidecars, update marimo's volume mount with HostToContainer propagation
@@ -280,6 +296,7 @@ func BuildPod(notebook *marimov1alpha1.MarimoNotebook) *corev1.Pod {
280296// buildSidecarContainer creates a container spec from a SidecarSpec.
281297// Sidecars share the PVC volume with the main marimo container.
282298// FUSE-based sidecars (with privileged security context) get Bidirectional mount propagation.
299+ // SSHFS sidecars (name starts with "sshfs-") get the ssh-pubkey secret mounted.
283300func buildSidecarContainer (sidecar marimov1alpha1.SidecarSpec , volumeMounts []corev1.VolumeMount ) corev1.Container {
284301 // Copy volume mounts so we can modify them for this container
285302 sidecarMounts := make ([]corev1.VolumeMount , len (volumeMounts ))
@@ -296,6 +313,16 @@ func buildSidecarContainer(sidecar marimov1alpha1.SidecarSpec, volumeMounts []co
296313 }
297314 }
298315
316+ // SSHFS sidecars need the ssh-pubkey secret for key-based auth
317+ // The linuxserver/openssh-server image reads PUBLIC_KEY_FILE from this path
318+ if strings .HasPrefix (sidecar .Name , "sshfs-" ) {
319+ sidecarMounts = append (sidecarMounts , corev1.VolumeMount {
320+ Name : "ssh-pubkey" ,
321+ MountPath : "/config/ssh-pubkey" ,
322+ ReadOnly : true ,
323+ })
324+ }
325+
299326 container := corev1.Container {
300327 Name : sidecar .Name ,
301328 Image : sidecar .Image ,
@@ -369,36 +396,6 @@ func applyPodOverrides(base, overrides corev1.PodSpec) corev1.PodSpec {
369396 return result
370397}
371398
372- // parseRemoteMountURI parses a remote mount URI with optional custom mount point.
373- // Format: scheme://user@host:/source or scheme://user@host:/source:/mount
374- // Returns: (userHost, sourcePath, mountPoint)
375- // If no custom mount point specified, mountPoint is empty.
376- func parseRemoteMountURI (uri , scheme string ) (userHost , sourcePath , mountPoint string ) {
377- trimmed := strings .TrimPrefix (uri , scheme + "://" )
378-
379- // Split at first : that follows a / to get user@host
380- colonIdx := strings .Index (trimmed , ":/" )
381- if colonIdx == - 1 {
382- return "" , "" , ""
383- }
384-
385- userHost = trimmed [:colonIdx ]
386- pathPart := trimmed [colonIdx + 1 :] // includes leading /
387-
388- // Check for custom mount point (another : followed by /)
389- // /data:/mnt → source=/data, mount=/mnt
390- lastColonIdx := strings .LastIndex (pathPart , ":/" )
391- if lastColonIdx > 0 {
392- sourcePath = pathPart [:lastColonIdx ]
393- mountPoint = pathPart [lastColonIdx + 1 :]
394- } else {
395- sourcePath = pathPart
396- mountPoint = ""
397- }
398-
399- return userHost , sourcePath , mountPoint
400- }
401-
402399// parseCWMountURI parses a cw:// URI for CoreWeave S3 mounts.
403400// Format: cw://bucket[/path][:mount_point]
404401// Returns: (bucket, subpath, mountPoint)
@@ -424,126 +421,26 @@ func parseCWMountURI(uri string) (bucket, subpath, mountPoint string) {
424421
425422// expandMounts converts mount URIs to sidecar specs.
426423// Supported schemes:
427- // - sshfs://user@host:/remote/path → SSHFS sidecar (requires FUSE)
428- // - sshfs://user@host:/remote/path:/mount → SSHFS with custom mount point
429- // - rsync://user@host:/remote/path → rsync sidecar (no FUSE, periodic sync)
430- // - rsync://user@host:/remote/path:/mount → rsync with custom mount point
431424// - cw://bucket/path → CoreWeave S3 sidecar using s3fs
432425// - cw://bucket/path:/mount → CoreWeave S3 with custom mount point
426+ //
427+ // Note: sshfs:// and rsync:// mounts are handled by the kubectl-marimo plugin,
428+ // not the operator. The plugin adds explicit sidecar specs to the CRD.
433429func expandMounts (mounts []string ) []marimov1alpha1.SidecarSpec {
434430 var sidecars []marimov1alpha1.SidecarSpec
435431
436432 for i , mount := range mounts {
437- if strings .HasPrefix (mount , "sshfs://" ) {
438- if sidecar := buildSSHFSSidecar (mount , i ); sidecar != nil {
439- sidecars = append (sidecars , * sidecar )
440- }
441- } else if strings .HasPrefix (mount , "rsync://" ) {
442- if sidecar := buildRsyncSidecar (mount , i ); sidecar != nil {
443- sidecars = append (sidecars , * sidecar )
444- }
445- } else if strings .HasPrefix (mount , "cw://" ) {
433+ if strings .HasPrefix (mount , "cw://" ) {
446434 if sidecar := buildCWSidecar (mount , i ); sidecar != nil {
447435 sidecars = append (sidecars , * sidecar )
448436 }
449437 }
438+ // sshfs:// and rsync:// are handled by plugin - ignore here
450439 }
451440
452441 return sidecars
453442}
454443
455- // buildSSHFSSidecar creates a sidecar spec for SSHFS mount.
456- // URI format: sshfs://user@host:/remote/path or sshfs://user@host:/remote/path:/mount
457- // The sidecar mounts the remote path to the specified mount point or default location.
458- func buildSSHFSSidecar (uri string , index int ) * marimov1alpha1.SidecarSpec {
459- userHost , remotePath , customMount := parseRemoteMountURI (uri , "sshfs" )
460- if userHost == "" || remotePath == "" {
461- return nil
462- }
463-
464- // Generate a unique name for the mount
465- mountName := fmt .Sprintf ("sshfs-%d" , index )
466-
467- // Use custom mount point or default to /home/marimo/notebooks/mounts/<name>
468- localMountPoint := customMount
469- if localMountPoint == "" {
470- localMountPoint = fmt .Sprintf ("%s/mounts/%s" , NotebookDir , mountName )
471- }
472-
473- return & marimov1alpha1.SidecarSpec {
474- Name : mountName ,
475- Image : config .AlpineImage ,
476- Command : []string {"sh" , "-c" },
477- Args : []string {
478- fmt .Sprintf (
479- "apk add --no-cache sshfs openssh-client && mkdir -p %s && " +
480- "sshfs -o StrictHostKeyChecking=no,UserKnownHostsFile=/dev/null," +
481- "reconnect,ServerAliveInterval=15,allow_other %s:%s %s && sleep infinity" ,
482- localMountPoint ,
483- userHost ,
484- remotePath ,
485- localMountPoint ,
486- ),
487- },
488- Env : []corev1.EnvVar {
489- // SSH key should be mounted from a secret named "ssh-credentials"
490- // The user can configure this via podOverrides if needed
491- },
492- // FUSE requires privileged access to /dev/fuse
493- SecurityContext : & corev1.SecurityContext {
494- Privileged : ptrBool (true ),
495- },
496- }
497- }
498-
499- // buildRsyncSidecar creates a sidecar spec for rsync-based file sync.
500- // URI format: rsync://user@host:/remote/path or rsync://user@host:/remote/path:/mount
501- // No FUSE required - works unprivileged.
502- // Behavior: initial sync from remote, then watches local changes and syncs back.
503- func buildRsyncSidecar (uri string , index int ) * marimov1alpha1.SidecarSpec {
504- userHost , remotePath , customMount := parseRemoteMountURI (uri , "rsync" )
505- if userHost == "" || remotePath == "" {
506- return nil
507- }
508-
509- mountName := fmt .Sprintf ("rsync-%d" , index )
510-
511- // Use custom mount point or default to /home/marimo/notebooks/mounts/<name>
512- localMountPoint := customMount
513- if localMountPoint == "" {
514- localMountPoint = fmt .Sprintf ("%s/mounts/%s" , NotebookDir , mountName )
515- }
516-
517- return & marimov1alpha1.SidecarSpec {
518- Name : mountName ,
519- Image : config .AlpineImage ,
520- Command : []string {"sh" , "-c" },
521- Args : []string {
522- fmt .Sprintf (
523- "apk add --no-cache openssh-client rsync inotify-tools && " +
524- "mkdir -p %s && " +
525- "echo 'Initial sync from %s:%s' && " +
526- "rsync -avz -e 'ssh -o StrictHostKeyChecking=no " +
527- "-o UserKnownHostsFile=/dev/null' %s:%s/ %s/ || " +
528- "echo 'Initial sync failed (check SSH credentials)' && " +
529- "echo 'Watching for changes...' && " +
530- "while inotifywait -r -e modify,create,delete %s 2>/dev/null; do " +
531- "rsync -avz -e 'ssh -o StrictHostKeyChecking=no " +
532- "-o UserKnownHostsFile=/dev/null' %s/ %s:%s/; " +
533- "done" ,
534- localMountPoint ,
535- userHost , remotePath ,
536- userHost , remotePath , localMountPoint ,
537- localMountPoint ,
538- localMountPoint , userHost , remotePath ,
539- ),
540- },
541- Env : []corev1.EnvVar {
542- // SSH key should be mounted from a secret named "ssh-credentials"
543- },
544- }
545- }
546-
547444// CWCredentialsSecret is the name of the K8s secret containing S3 credentials.
548445// The kubectl-marimo plugin auto-creates this from ~/.s3cfg.
549446const CWCredentialsSecret = "cw-credentials"
0 commit comments