Skip to content

Commit 9903ab0

Browse files
committed
run marimo and init containers as the same user
We just assume that user 1000 is the correct default user; you can always override the user if you provide a custom image.
1 parent bb27711 commit 9903ab0

6 files changed

Lines changed: 123 additions & 20 deletions

File tree

api/v1alpha1/marimonotebook_types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ type MarimoNotebookSpec struct {
8787
// +optional
8888
Image string `json:"image,omitempty"`
8989

90+
// User id for containers in the marimo pod
91+
// +kubebuilder:default:=1000
92+
// +kubebuilder:validation:Minimum=0
93+
// +optional
94+
RunAsUser *int64 `json:"runAsUser,omitempty"`
95+
9096
// Port for marimo server
9197
// +kubebuilder:default:=2718
9298
// +kubebuilder:validation:Minimum=1

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/marimo.io_marimos.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,12 @@ spec:
303303
description: Requests specifies minimum resources required
304304
type: object
305305
type: object
306+
runAsUser:
307+
default: 1000
308+
description: User id for containers in the marimo pod
309+
format: int64
310+
minimum: 0
311+
type: integer
306312
sidecars:
307313
description: |-
308314
Sidecars are additional containers that run alongside marimo

deploy/install.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,12 @@ spec:
311311
description: Requests specifies minimum resources required
312312
type: object
313313
type: object
314+
runAsUser:
315+
default: 1000
316+
description: User id for containers in the marimo pod
317+
format: int64
318+
minimum: 0
319+
type: integer
314320
sidecars:
315321
description: |-
316322
Sidecars are additional containers that run alongside marimo

pkg/resources/pod.go

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ func BuildPod(notebook *marimov1alpha1.MarimoNotebook) *corev1.Pod {
2929
var volumeMounts []corev1.VolumeMount
3030
var volumes []corev1.Volume
3131

32+
runAsUser := notebook.Spec.RunAsUser
33+
if runAsUser == nil {
34+
defaultUID := int64(1000)
35+
runAsUser = &defaultUID
36+
}
37+
3238
// Use PVC if storage is configured, otherwise emptyDir
3339
if notebook.Spec.Storage != nil {
3440
volumes = []corev1.Volume{
@@ -82,6 +88,7 @@ func BuildPod(notebook *marimov1alpha1.MarimoNotebook) *corev1.Pod {
8288
{Name: PVCVolumeName, MountPath: NotebookDir},
8389
{Name: ConfigMapVolumeName, MountPath: "/content", ReadOnly: true},
8490
},
91+
SecurityContext: withRunAsUser(nil, runAsUser),
8592
},
8693
}
8794
} else if notebook.Spec.Source != "" {
@@ -99,6 +106,7 @@ func BuildPod(notebook *marimov1alpha1.MarimoNotebook) *corev1.Pod {
99106
VolumeMounts: []corev1.VolumeMount{
100107
{Name: PVCVolumeName, MountPath: NotebookDir},
101108
},
109+
SecurityContext: withRunAsUser(nil, runAsUser),
102110
},
103111
}
104112
}
@@ -114,6 +122,7 @@ func BuildPod(notebook *marimov1alpha1.MarimoNotebook) *corev1.Pod {
114122
VolumeMounts: []corev1.VolumeMount{
115123
{Name: "venv", MountPath: "/opt/venv"},
116124
},
125+
SecurityContext: withRunAsUser(nil, runAsUser),
117126
})
118127

119128
// Add venv volume (emptyDir, shared between init and main container)
@@ -201,7 +210,7 @@ func BuildPod(notebook *marimov1alpha1.MarimoNotebook) *corev1.Pod {
201210

202211
// Expand mounts to sidecars and merge with explicit sidecars
203212
// (do this first so we can check for FUSE sidecars)
204-
allSidecars := expandMounts(notebook.Spec.Mounts)
213+
allSidecars := expandMounts(notebook.Spec.Mounts, runAsUser)
205214
allSidecars = append(allSidecars, notebook.Spec.Sidecars...)
206215

207216
// Check if any sidecar uses FUSE (privileged) - if so, marimo container needs
@@ -261,8 +270,9 @@ func BuildPod(notebook *marimov1alpha1.MarimoNotebook) *corev1.Pod {
261270
Protocol: corev1.ProtocolTCP,
262271
},
263272
},
264-
VolumeMounts: marimoVolumeMounts,
265-
Resources: buildResourceRequirements(notebook.Spec.Resources),
273+
VolumeMounts: marimoVolumeMounts,
274+
Resources: buildResourceRequirements(notebook.Spec.Resources),
275+
SecurityContext: withRunAsUser(nil, runAsUser),
266276
},
267277
}
268278

@@ -426,12 +436,12 @@ func parseCWMountURI(uri string) (bucket, subpath, mountPoint string) {
426436
//
427437
// Note: sshfs:// and rsync:// mounts are handled by the kubectl-marimo plugin,
428438
// not the operator. The plugin adds explicit sidecar specs to the CRD.
429-
func expandMounts(mounts []string) []marimov1alpha1.SidecarSpec {
439+
func expandMounts(mounts []string, runAsUser *int64) []marimov1alpha1.SidecarSpec {
430440
var sidecars []marimov1alpha1.SidecarSpec
431441

432442
for i, mount := range mounts {
433443
if strings.HasPrefix(mount, "cw://") {
434-
if sidecar := buildCWSidecar(mount, i); sidecar != nil {
444+
if sidecar := buildCWSidecar(mount, i, runAsUser); sidecar != nil {
435445
sidecars = append(sidecars, *sidecar)
436446
}
437447
}
@@ -449,7 +459,7 @@ const CWCredentialsSecret = "cw-credentials"
449459
// URI format: cw://bucket[/path][:mount]
450460
// Credentials from cw-credentials secret (auto-created by kubectl-marimo plugin).
451461
// Endpoint from S3_ENDPOINT env var (default: https://cwobject.com).
452-
func buildCWSidecar(uri string, index int) *marimov1alpha1.SidecarSpec {
462+
func buildCWSidecar(uri string, index int, runAsUser *int64) *marimov1alpha1.SidecarSpec {
453463
bucket, subpath, customMount := parseCWMountURI(uri)
454464
if bucket == "" {
455465
return nil
@@ -509,9 +519,12 @@ func buildCWSidecar(uri string, index int) *marimov1alpha1.SidecarSpec {
509519
},
510520
},
511521
// FUSE requires privileged access to /dev/fuse
512-
SecurityContext: &corev1.SecurityContext{
513-
Privileged: ptrBool(true),
514-
},
522+
SecurityContext: withRunAsUser(
523+
&corev1.SecurityContext{
524+
Privileged: ptrBool(true),
525+
},
526+
runAsUser,
527+
),
515528
}
516529
}
517530

@@ -520,7 +533,17 @@ func ptrBool(b bool) *bool {
520533
return &b
521534
}
522535

523-
// ptrString returns a pointer to a string value.
524-
func ptrString(s string) *string {
525-
return &s
536+
// withRunAsUser returns a SecurityContext with RunAsUser set.
537+
// Returns nil if both arguments are nil.
538+
// If existing is non-nil it is shallow-copied so the original is not modified.
539+
func withRunAsUser(existing *corev1.SecurityContext, runAsUser *int64) *corev1.SecurityContext {
540+
if existing == nil && runAsUser == nil {
541+
return nil
542+
}
543+
if existing == nil {
544+
return &corev1.SecurityContext{RunAsUser: runAsUser}
545+
}
546+
sc := *existing
547+
sc.RunAsUser = runAsUser
548+
return &sc
526549
}

pkg/resources/pod_test.go

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ const (
2020
testSSHPubkeyName = "ssh-pubkey"
2121
)
2222

23+
func ptrString(s string) *string { return &s }
24+
func ptrInt64(i int64) *int64 { return &i }
25+
2326
func TestBuildPod_BasicConfig(t *testing.T) {
2427
notebook := &marimov1alpha1.MarimoNotebook{
2528
ObjectMeta: metav1.ObjectMeta{
@@ -1069,13 +1072,67 @@ func TestBuildPod_EnvVarsEmpty(t *testing.T) {
10691072
}
10701073
}
10711074

1075+
func TestBuildPod_RunAsUser(t *testing.T) {
1076+
var runAsUser int64 = 1234
1077+
notebook := &marimov1alpha1.MarimoNotebook{
1078+
ObjectMeta: metav1.ObjectMeta{
1079+
Name: testNotebookName,
1080+
Namespace: testNamespace,
1081+
},
1082+
Spec: marimov1alpha1.MarimoNotebookSpec{
1083+
Image: "ghcr.io/marimo-team/marimo:latest",
1084+
Port: 2718,
1085+
Source: "https://github.com/marimo-team/marimo.git",
1086+
RunAsUser: &runAsUser,
1087+
},
1088+
}
1089+
1090+
pod := BuildPod(notebook)
1091+
1092+
// Check marimo main container has RunAsUser set
1093+
var marimoContainer *corev1.Container
1094+
for i := range pod.Spec.Containers {
1095+
if pod.Spec.Containers[i].Name == testMarimoContainer {
1096+
marimoContainer = &pod.Spec.Containers[i]
1097+
break
1098+
}
1099+
}
1100+
if marimoContainer == nil {
1101+
t.Fatal("marimo container not found")
1102+
}
1103+
if marimoContainer.SecurityContext == nil || marimoContainer.SecurityContext.RunAsUser == nil {
1104+
t.Fatal("marimo container SecurityContext.RunAsUser not set")
1105+
}
1106+
if *marimoContainer.SecurityContext.RunAsUser != runAsUser {
1107+
t.Errorf("marimo container RunAsUser: expected %d, got %d", runAsUser, *marimoContainer.SecurityContext.RunAsUser)
1108+
}
1109+
1110+
// Check git-clone init container has RunAsUser set
1111+
var gitCloneContainer *corev1.Container
1112+
for i := range pod.Spec.InitContainers {
1113+
if pod.Spec.InitContainers[i].Name == "git-clone" {
1114+
gitCloneContainer = &pod.Spec.InitContainers[i]
1115+
break
1116+
}
1117+
}
1118+
if gitCloneContainer == nil {
1119+
t.Fatal("git-clone init container not found")
1120+
}
1121+
if gitCloneContainer.SecurityContext == nil || gitCloneContainer.SecurityContext.RunAsUser == nil {
1122+
t.Fatal("git-clone container SecurityContext.RunAsUser not set")
1123+
}
1124+
if *gitCloneContainer.SecurityContext.RunAsUser != runAsUser {
1125+
t.Errorf("git-clone container RunAsUser: expected %d, got %d", runAsUser, *gitCloneContainer.SecurityContext.RunAsUser)
1126+
}
1127+
}
1128+
10721129
func TestExpandMounts_SSHFSIgnored(t *testing.T) {
10731130
// sshfs:// mounts are handled by the plugin, not the operator
10741131
mounts := []string{
10751132
"sshfs:///home/marimo/notebooks",
10761133
}
10771134

1078-
sidecars := expandMounts(mounts)
1135+
sidecars := expandMounts(mounts, nil)
10791136

10801137
// sshfs:// should be ignored (plugin handles it)
10811138
if len(sidecars) != 0 {
@@ -1089,7 +1146,7 @@ func TestExpandMounts_UnsupportedScheme(t *testing.T) {
10891146
"nfs://server/path", // Not supported yet
10901147
}
10911148

1092-
sidecars := expandMounts(mounts)
1149+
sidecars := expandMounts(mounts, nil)
10931150

10941151
// Should return empty - unsupported schemes are ignored
10951152
if len(sidecars) != 0 {
@@ -1103,7 +1160,7 @@ func TestExpandMounts_RsyncIgnored(t *testing.T) {
11031160
"rsync://./local/data",
11041161
}
11051162

1106-
sidecars := expandMounts(mounts)
1163+
sidecars := expandMounts(mounts, nil)
11071164

11081165
// rsync:// should be ignored (plugin handles it)
11091166
if len(sidecars) != 0 {
@@ -1119,7 +1176,7 @@ func TestExpandMounts_MixedSchemes(t *testing.T) {
11191176
"cw://bucket/path3",
11201177
}
11211178

1122-
sidecars := expandMounts(mounts)
1179+
sidecars := expandMounts(mounts, nil)
11231180

11241181
// Only cw:// should produce sidecar
11251182
if len(sidecars) != 1 {
@@ -1203,7 +1260,7 @@ func TestParseCWMountURI(t *testing.T) {
12031260
func TestExpandMounts_CW(t *testing.T) {
12041261
mounts := []string{"cw://mybucket/data"}
12051262

1206-
sidecars := expandMounts(mounts)
1263+
sidecars := expandMounts(mounts, nil)
12071264

12081265
if len(sidecars) != 1 {
12091266
t.Fatalf("expected 1 sidecar, got %d", len(sidecars))
@@ -1236,7 +1293,7 @@ func TestExpandMounts_CW(t *testing.T) {
12361293
func TestExpandMounts_CWCustomMountPoint(t *testing.T) {
12371294
mounts := []string{"cw://mybucket/data:/mnt/s3"}
12381295

1239-
sidecars := expandMounts(mounts)
1296+
sidecars := expandMounts(mounts, nil)
12401297

12411298
if len(sidecars) != 1 {
12421299
t.Fatalf("expected 1 sidecar, got %d", len(sidecars))
@@ -1255,7 +1312,7 @@ func TestExpandMounts_CWCustomMountPoint(t *testing.T) {
12551312
func TestExpandMounts_CWBucketOnly(t *testing.T) {
12561313
mounts := []string{"cw://mybucket"}
12571314

1258-
sidecars := expandMounts(mounts)
1315+
sidecars := expandMounts(mounts, nil)
12591316

12601317
if len(sidecars) != 1 {
12611318
t.Fatalf("expected 1 sidecar, got %d", len(sidecars))
@@ -1275,7 +1332,7 @@ func TestExpandMounts_CWBucketOnly(t *testing.T) {
12751332
func TestExpandMounts_CWSecretReference(t *testing.T) {
12761333
mounts := []string{"cw://mybucket/data"}
12771334

1278-
sidecars := expandMounts(mounts)
1335+
sidecars := expandMounts(mounts, nil)
12791336

12801337
if len(sidecars) != 1 {
12811338
t.Fatalf("expected 1 sidecar, got %d", len(sidecars))

0 commit comments

Comments
 (0)