Skip to content

Commit 5f1b56b

Browse files
committed
refactor(operator): remove pvc-based storage paths
1 parent f61430d commit 5f1b56b

7 files changed

Lines changed: 166 additions & 682 deletions

File tree

docs/2026-02-24-simplest-spritz-deployment-spec.md

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ The default installation should require only:
6666
- `global.ingress.className`: ingress class
6767
- `global.ingress.tls.enabled`: whether TLS is enabled
6868
- `global.ingress.tls.secretName` (optional): pre-provisioned TLS secret name
69-
- `operator.homePVC.storageClass` (optional): home PVC storage class override
7069

7170
Everything else should have working defaults.
7271

@@ -98,10 +97,6 @@ ui:
9897
apiBaseUrl: /api
9998

10099
operator:
101-
homePVC:
102-
enabled: true
103-
storageClassName: standard
104-
105100
sharedMounts:
106101
enabled: false
107102

@@ -150,7 +145,6 @@ File: `helm/spritz/values.yaml`
150145
- Add `global.ingress.tls.secretName` (default empty; operator-provided).
151146
- Keep `ui.ingress.enabled` default `true` for single-host installs.
152147
- Keep `ui.apiBaseUrl` default `/api`.
153-
- Keep `operator.homePVC.enabled` default `true`.
154148
- Keep `operator.sharedMounts.enabled` and `api.sharedMounts.enabled` default `false`.
155149
- Remove compatibility-only keys from the default path:
156150
- `ui.ingress.host`
@@ -224,15 +218,15 @@ Required behavior:
224218

225219
## Storage and Sync Defaults
226220

227-
- Default mode is per-devbox persistent home PVC.
221+
- Default mode is ephemeral home storage (`EmptyDir` at `/home/dev`).
228222
- Shared cross-devbox live sync is disabled by default.
229223
- Shared mounts remain available as an opt-in advanced feature.
230224

231225
Rationale:
232226

233-
- PVC-only mode has fewer failure modes.
234-
- This is enough for most single-devbox usage.
235-
- Operators can enable shared sync only when they need it.
227+
- Ephemeral defaults keep install and cleanup behavior predictable.
228+
- This is enough for stateless single-devbox usage.
229+
- Operators can enable shared sync only for specific paths they need to persist.
236230

237231
## Optional Advanced Mode
238232

helm/spritz/templates/operator-deployment.yaml

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -43,38 +43,6 @@ spec:
4343
- name: SPRITZ_HOME_SIZE_LIMIT
4444
value: {{ .Values.operator.homeSizeLimit | quote }}
4545
{{- end }}
46-
{{- if and .Values.operator.homePVC .Values.operator.homePVC.enabled }}
47-
- name: SPRITZ_HOME_PVC_PREFIX
48-
value: {{ .Values.operator.homePVC.prefix | quote }}
49-
- name: SPRITZ_HOME_PVC_SIZE
50-
value: {{ .Values.operator.homePVC.size | quote }}
51-
- name: SPRITZ_HOME_PVC_ACCESS_MODES
52-
value: {{ join "," .Values.operator.homePVC.accessModes | quote }}
53-
{{- if .Values.operator.homePVC.storageClass }}
54-
- name: SPRITZ_HOME_PVC_STORAGE_CLASS
55-
value: {{ .Values.operator.homePVC.storageClass | quote }}
56-
{{- end }}
57-
{{- if .Values.operator.homePVC.mountPaths }}
58-
- name: SPRITZ_HOME_MOUNT_PATHS
59-
value: {{ join "," .Values.operator.homePVC.mountPaths | quote }}
60-
{{- end }}
61-
{{- end }}
62-
{{- if and .Values.operator.sharedConfigPVC .Values.operator.sharedConfigPVC.enabled }}
63-
- name: SPRITZ_SHARED_CONFIG_PVC_PREFIX
64-
value: {{ .Values.operator.sharedConfigPVC.prefix | quote }}
65-
- name: SPRITZ_SHARED_CONFIG_PVC_SIZE
66-
value: {{ .Values.operator.sharedConfigPVC.size | quote }}
67-
- name: SPRITZ_SHARED_CONFIG_PVC_ACCESS_MODES
68-
value: {{ join "," .Values.operator.sharedConfigPVC.accessModes | quote }}
69-
{{- if .Values.operator.sharedConfigPVC.storageClass }}
70-
- name: SPRITZ_SHARED_CONFIG_PVC_STORAGE_CLASS
71-
value: {{ .Values.operator.sharedConfigPVC.storageClass | quote }}
72-
{{- end }}
73-
{{- if .Values.operator.sharedConfigPVC.mountPath }}
74-
- name: SPRITZ_SHARED_CONFIG_MOUNT_PATH
75-
value: {{ .Values.operator.sharedConfigPVC.mountPath | quote }}
76-
{{- end }}
77-
{{- end }}
7846
{{- if and .Values.operator.sharedMounts .Values.operator.sharedMounts.enabled }}
7947
- name: SPRITZ_SHARED_MOUNTS
8048
value: {{ toJson .Values.operator.sharedMounts.mounts | quote }}

helm/spritz/values.yaml

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -77,23 +77,6 @@ operator:
7777
workspaceSizeLimit: 10Gi
7878
homeSizeLimit: 5Gi
7979
podNodeSelector: ""
80-
homePVC:
81-
enabled: true
82-
prefix: spritz-home
83-
size: 5Gi
84-
accessModes:
85-
- ReadWriteOnce
86-
storageClass: ""
87-
mountPaths:
88-
- /home/dev
89-
sharedConfigPVC:
90-
enabled: false
91-
prefix: spritz-shared-config
92-
size: 100Mi
93-
accessModes:
94-
- ReadWriteMany
95-
storageClass: ""
96-
mountPath: /shared
9780
sharedMounts:
9881
enabled: false
9982
mounts: []
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package controllers
2+
3+
import (
4+
"testing"
5+
6+
corev1 "k8s.io/api/core/v1"
7+
8+
spritzv1 "spritz.sh/operator/api/v1"
9+
)
10+
11+
func TestParseCSV(t *testing.T) {
12+
if parseCSV("") != nil {
13+
t.Fatal("expected nil for empty CSV")
14+
}
15+
got := parseCSV("/home/dev, /home/spritz")
16+
if len(got) != 2 {
17+
t.Fatalf("expected 2 entries, got %d", len(got))
18+
}
19+
if got[0] != "/home/dev" || got[1] != "/home/spritz" {
20+
t.Fatalf("unexpected values: %v", got)
21+
}
22+
}
23+
24+
func TestParseNodeSelector(t *testing.T) {
25+
selector, err := parseNodeSelector("spritz.sh/storage-ready=true,zone=fsn1")
26+
if err != nil {
27+
t.Fatalf("unexpected error: %v", err)
28+
}
29+
if selector["spritz.sh/storage-ready"] != "true" || selector["zone"] != "fsn1" {
30+
t.Fatalf("unexpected selector: %v", selector)
31+
}
32+
33+
if _, err := parseNodeSelector("missingequals"); err == nil {
34+
t.Fatal("expected error for invalid selector entry")
35+
}
36+
if _, err := parseNodeSelector("=novalue"); err == nil {
37+
t.Fatal("expected error for empty key")
38+
}
39+
if _, err := parseNodeSelector("key="); err == nil {
40+
t.Fatal("expected error for empty value")
41+
}
42+
}
43+
44+
func TestBuildHomeMountsDefault(t *testing.T) {
45+
mounts := buildHomeMounts()
46+
if len(mounts) != 1 {
47+
t.Fatalf("expected 1 mount, got %d", len(mounts))
48+
}
49+
if mounts[0].Name != "home" {
50+
t.Fatalf("unexpected mount name: %s", mounts[0].Name)
51+
}
52+
if mounts[0].MountPath != repoInitHomeDir {
53+
t.Fatalf("unexpected mount path: %s", mounts[0].MountPath)
54+
}
55+
}
56+
57+
func TestBuildPodSecurityContext(t *testing.T) {
58+
if ctx := buildPodSecurityContext(false, false); ctx != nil {
59+
t.Fatal("expected nil security context when no shared mounts or repo init")
60+
}
61+
62+
ctx := buildPodSecurityContext(true, false)
63+
if ctx == nil || ctx.FSGroup == nil || *ctx.FSGroup != repoInitGroupID {
64+
t.Fatalf("expected fsGroup %d when shared mounts enabled, got %+v", repoInitGroupID, ctx)
65+
}
66+
67+
ctx = buildPodSecurityContext(false, true)
68+
if ctx == nil || ctx.FSGroup == nil || *ctx.FSGroup != repoInitGroupID {
69+
t.Fatalf("expected fsGroup %d when repo init present, got %+v", repoInitGroupID, ctx)
70+
}
71+
}
72+
73+
func TestBuildRepoInitContainerDedupesHomeMount(t *testing.T) {
74+
spritz := &spritzv1.Spritz{
75+
Spec: spritzv1.SpritzSpec{
76+
Repo: &spritzv1.SpritzRepo{
77+
URL: "https://github.com/example/repo.git",
78+
},
79+
},
80+
}
81+
82+
homeMounts := buildHomeMounts()
83+
repos := repoEntries(spritz)
84+
containers, _, err := buildRepoInitContainers(spritz, repos, homeMounts)
85+
if err != nil {
86+
t.Fatalf("unexpected error: %v", err)
87+
}
88+
if len(containers) == 0 {
89+
t.Fatal("expected repo init container")
90+
}
91+
92+
count := 0
93+
for _, mount := range containers[0].VolumeMounts {
94+
if mount.MountPath == repoInitHomeDir {
95+
count++
96+
}
97+
}
98+
if count != 1 {
99+
t.Fatalf("expected single %s mount, got %d", repoInitHomeDir, count)
100+
}
101+
}
102+
103+
func TestRepoDirNeedsWorkspaceMountHonorsSharedMounts(t *testing.T) {
104+
mountRoots := []corev1.VolumeMount{
105+
{Name: "shared", MountPath: "/shared"},
106+
}
107+
if repoDirNeedsWorkspaceMount("/shared/repo", mountRoots) {
108+
t.Fatal("expected repo dir under shared mount to skip workspace mount")
109+
}
110+
if repoDirNeedsWorkspaceMount("/workspace/repo", mountRoots) {
111+
t.Fatal("expected repo dir under /workspace to skip workspace mount")
112+
}
113+
}
114+
115+
func TestValidateRepoDir(t *testing.T) {
116+
cases := []struct {
117+
name string
118+
dir string
119+
wantErr bool
120+
}{
121+
{"empty ok", "", false},
122+
{"relative ok", "spritz", false},
123+
{"relative nested ok", "project/app", false},
124+
{"relative up invalid", "../etc", true},
125+
{"relative up nested invalid", "foo/../../etc", true},
126+
{"absolute workspace ok", "/workspace/spritz", false},
127+
{"absolute workspace nested ok", "/workspace/spritz/app", false},
128+
{"absolute escape invalid", "/etc", true},
129+
{"absolute escape via traversal invalid", "/workspace/../etc", true},
130+
}
131+
132+
for _, tc := range cases {
133+
t.Run(tc.name, func(t *testing.T) {
134+
err := validateRepoDir(tc.dir)
135+
if tc.wantErr && err == nil {
136+
t.Fatalf("expected error for %s", tc.dir)
137+
}
138+
if !tc.wantErr && err != nil {
139+
t.Fatalf("unexpected error for %s: %v", tc.dir, err)
140+
}
141+
})
142+
}
143+
}

0 commit comments

Comments
 (0)