Skip to content

Commit c62f9e0

Browse files
committed
refactor(config): use native k8s volumes for workspace storage
1 parent e906507 commit c62f9e0

14 files changed

Lines changed: 212 additions & 363 deletions

docs/config-manifest.md

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ spec: {}
2828
- `spec.namespace` (`string`, default: `default`)
2929
- `spec.kubeContext` (`string`, optional): kubeconfig context used by okdev commands.
3030
- `spec.session` (`object`)
31-
- `spec.workspace` (`object`, required)
31+
- `spec.volumes` (`array`, optional)
3232
- `spec.sync` (`object`)
3333
- `spec.ports` (`array`)
3434
- `spec.ssh` (`object`)
@@ -52,13 +52,21 @@ Context precedence:
5252
- `spec.kubeContext` from manifest
5353
- active kubeconfig current-context (default client behavior)
5454

55-
## `spec.workspace`
55+
## `spec.volumes`
5656

57-
- `mountPath` (`string`, required): workspace mount path in containers.
58-
- `pvc` (`object`):
59-
- `claimName` (`string`, optional): existing PVC to bind.
60-
- `size` (`string`, default: `50Gi`): requested PVC size.
61-
- `storageClassName` (`string`, optional)
57+
`spec.volumes` uses native Kubernetes `corev1.Volume` schema.
58+
59+
- Define storage source with standard `VolumeSource` (for example `emptyDir`, `persistentVolumeClaim`, `ephemeral`, `configMap`, `secret`).
60+
- Mount points are defined with standard Kubernetes `volumeMounts` in `spec.podTemplate.spec.containers[*].volumeMounts`.
61+
62+
Workspace behavior:
63+
- If a `workspace` volume is not provided, okdev injects:
64+
- `name: workspace`
65+
- `emptyDir: {}`
66+
- okdev ensures `workspace` is mounted on:
67+
- `dev` container
68+
- `okdev-sidecar`
69+
- Workspace mount path defaults to `/workspace` (or follows `dev` container `volumeMounts` entry for `workspace` if provided in `podTemplate`).
6270

6371
## `spec.sync`
6472

@@ -95,8 +103,8 @@ Validation:
95103
- `privateKeyPath` (`string`, optional)
96104
- `autoDetectPorts` (`bool`, default: `true`)
97105
- `persistentSession` (`bool`, default: `false`) enables tmux-backed interactive session mode
98-
- `keepAliveIntervalSeconds` (`int`, default: `30`)
99-
- `keepAliveTimeoutSeconds` (`int`, default: `90`)
106+
- `keepAliveIntervalSeconds` (`int`, default: `10`)
107+
- `keepAliveTimeoutSeconds` (`int`, default: `10`)
100108

101109
Validation:
102110
- `remotePort` must be `1..65535`
@@ -139,11 +147,14 @@ spec:
139147
ttlHours: 72
140148
idleTimeoutMinutes: 120
141149
shareable: true
142-
workspace:
143-
mountPath: /workspace
144-
pvc:
145-
size: 200Gi
146-
storageClassName: fast-ssd
150+
mounts:
151+
volumes:
152+
- name: workspace
153+
persistentVolumeClaim:
154+
claimName: team-workspace
155+
- name: datasets
156+
persistentVolumeClaim:
157+
claimName: team-datasets
147158
sync:
148159
engine: syncthing
149160
syncthing:
@@ -179,6 +190,12 @@ spec:
179190
- name: dev
180191
image: nvidia/cuda:12.4.1-devel-ubuntu22.04
181192
command: ["sleep", "infinity"]
193+
volumeMounts:
194+
- name: workspace
195+
mountPath: /workspace
196+
- name: datasets
197+
mountPath: /data
198+
readOnly: true
182199
resources:
183200
requests:
184201
cpu: "8"

internal/cli/common.go

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -174,23 +174,6 @@ func podName(sessionName string) string {
174174
return "okdev-" + sessionName
175175
}
176176

177-
func pvcName(cfg *config.DevEnvironment, sessionName string) string {
178-
if cfg.Spec.Workspace.PVC.ClaimName != "" {
179-
return cfg.Spec.Workspace.PVC.ClaimName
180-
}
181-
return "okdev-" + sessionName + "-workspace"
182-
}
183-
184-
func usesWorkspacePVC(cfg *config.DevEnvironment) bool {
185-
if cfg == nil {
186-
return false
187-
}
188-
pvc := cfg.Spec.Workspace.PVC
189-
return strings.TrimSpace(pvc.ClaimName) != "" ||
190-
strings.TrimSpace(pvc.Size) != "" ||
191-
strings.TrimSpace(pvc.StorageClassName) != ""
192-
}
193-
194177
func newKubeClient(opts *Options) *kube.Client {
195178
return &kube.Client{Context: opts.Context}
196179
}

internal/cli/common_test.go

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,28 +26,6 @@ func TestNamesAndLabels(t *testing.T) {
2626
}
2727
}
2828

29-
func TestPVCName(t *testing.T) {
30-
cfg := &config.DevEnvironment{}
31-
if usesWorkspacePVC(cfg) {
32-
t.Fatal("expected pvc disabled when workspace.pvc is not configured")
33-
}
34-
cfg.Spec.Workspace.PVC.Size = "50Gi"
35-
if !usesWorkspacePVC(cfg) {
36-
t.Fatal("expected pvc enabled when size is configured")
37-
}
38-
if pvcName(cfg, "s1") != "okdev-s1-workspace" {
39-
t.Fatal("unexpected managed pvc name")
40-
}
41-
cfg.Spec.Workspace.PVC.Size = ""
42-
cfg.Spec.Workspace.PVC.ClaimName = "existing"
43-
if !usesWorkspacePVC(cfg) {
44-
t.Fatal("expected pvc enabled for explicit claim")
45-
}
46-
if pvcName(cfg, "s1") != "existing" {
47-
t.Fatal("expected explicit claim name")
48-
}
49-
}
50-
5129
func TestAnnotationsForSession(t *testing.T) {
5230
cfg := &config.DevEnvironment{}
5331
cfg.Spec.Session.TTLHours = 72

internal/cli/down.go

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ func newDownCmd(opts *Options) *cobra.Command {
3737
ui.stepDone("session", sn)
3838
ui.stepDone("namespace", ns)
3939
k := newKubeClient(opts)
40-
usePVC := usesWorkspacePVC(cfg)
4140
if err := ensureSessionOwnership(opts, k, ns, sn, false); err != nil {
4241
return err
4342
}
@@ -48,10 +47,8 @@ func newDownCmd(opts *Options) *cobra.Command {
4847
ui.section("Dry Run")
4948
fmt.Fprintf(cmd.OutOrStdout(), "DRY RUN: session=%s namespace=%s\n", sn, ns)
5049
fmt.Fprintf(cmd.OutOrStdout(), "- would delete pod/%s\n", podName(sn))
51-
if deletePVC && usePVC && cfg.Spec.Workspace.PVC.ClaimName == "" {
52-
fmt.Fprintf(cmd.OutOrStdout(), "- would delete pvc/%s\n", pvcName(cfg, sn))
53-
} else if !deletePVC && usePVC && cfg.Spec.Workspace.PVC.ClaimName == "" {
54-
fmt.Fprintf(cmd.OutOrStdout(), "- would retain pvc/%s\n", pvcName(cfg, sn))
50+
if deletePVC {
51+
fmt.Fprintln(cmd.OutOrStdout(), "- note: --delete-pvc is ignored (okdev no longer manages PVC lifecycle)")
5552
}
5653
return nil
5754
}
@@ -62,19 +59,10 @@ func newDownCmd(opts *Options) *cobra.Command {
6259
return fmt.Errorf("delete session pod: %w", err)
6360
}
6461
ui.stepDone("pod", "deleted")
65-
if deletePVC && usePVC && cfg.Spec.Workspace.PVC.ClaimName == "" {
66-
ui.stepRun("pvc", pvcName(cfg, sn))
67-
if err := k.Delete(ctx, ns, "pvc", pvcName(cfg, sn), true); err != nil && !apierrors.IsNotFound(err) {
68-
return fmt.Errorf("delete workspace pvc: %w", err)
69-
}
70-
ui.stepDone("pvc", "deleted")
71-
} else if usePVC && cfg.Spec.Workspace.PVC.ClaimName == "" {
72-
ui.stepDone("pvc", "retained")
73-
} else if usePVC {
74-
ui.stepDone("pvc", "external claim")
75-
} else {
76-
ui.stepDone("pvc", "not used")
62+
if deletePVC {
63+
ui.warnf("--delete-pvc ignored: okdev no longer manages PVC lifecycle; delete PVCs manually if needed")
7764
}
65+
ui.stepDone("pvc", "not managed")
7866
alias := sshHostAlias(sn)
7967
ui.section("Cleanup")
8068
_ = stopManagedSSHForward(alias)
@@ -93,16 +81,7 @@ func newDownCmd(opts *Options) *cobra.Command {
9381
fmt.Fprintf(cmd.OutOrStdout(), "session: %s\n", sn)
9482
fmt.Fprintf(cmd.OutOrStdout(), "namespace: %s\n", ns)
9583
fmt.Fprintln(cmd.OutOrStdout(), "status: stopped")
96-
if !deletePVC && usePVC && cfg.Spec.Workspace.PVC.ClaimName == "" {
97-
fmt.Fprintf(cmd.OutOrStdout(), "workspace: retained (%s)\n", pvcName(cfg, sn))
98-
fmt.Fprintln(cmd.OutOrStdout(), "note: use --delete-pvc to remove workspace storage")
99-
} else if deletePVC && usePVC && cfg.Spec.Workspace.PVC.ClaimName == "" {
100-
fmt.Fprintln(cmd.OutOrStdout(), "workspace: deleted")
101-
} else if usePVC {
102-
fmt.Fprintf(cmd.OutOrStdout(), "workspace: external claim (%s)\n", cfg.Spec.Workspace.PVC.ClaimName)
103-
} else {
104-
fmt.Fprintln(cmd.OutOrStdout(), "workspace: ephemeral (emptyDir)")
105-
}
84+
fmt.Fprintln(cmd.OutOrStdout(), "workspace: pod deleted; volumes/PVCs unchanged")
10685
return nil
10786
},
10887
}

internal/cli/lifecycle.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func resolvePostCreateCommand(cfg *config.DevEnvironment, configPath string) str
1818
}
1919
script := filepath.Join(root, ".okdev", "post-create.sh")
2020
if st, err := os.Stat(script); err == nil && !st.IsDir() {
21-
return filepath.Join(cfg.Spec.Workspace.MountPath, ".okdev", "post-create.sh")
21+
return filepath.Join(cfg.WorkspaceMountPath(), ".okdev", "post-create.sh")
2222
}
2323
return ""
2424
}
@@ -33,7 +33,7 @@ func resolvePreStopCommand(cfg *config.DevEnvironment, configPath string) string
3333
}
3434
script := filepath.Join(root, ".okdev", "pre-stop.sh")
3535
if st, err := os.Stat(script); err == nil && !st.IsDir() {
36-
return filepath.Join(cfg.Spec.Workspace.MountPath, ".okdev", "pre-stop.sh")
36+
return filepath.Join(cfg.WorkspaceMountPath(), ".okdev", "pre-stop.sh")
3737
}
3838
return ""
3939
}

internal/cli/lifecycle_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
func testLifecycleCfg() *config.DevEnvironment {
1212
return &config.DevEnvironment{
1313
Spec: config.DevEnvSpec{
14-
Workspace: config.Workspace{MountPath: "/workspace"},
14+
Volumes: nil,
1515
},
1616
}
1717
}

internal/cli/sync.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func newSyncCmd(opts *Options) *cobra.Command {
4747
if engine != "syncthing" {
4848
return fmt.Errorf("unsupported sync engine %q (only syncthing is supported)", engine)
4949
}
50-
pairs, err := syncengine.ParsePairs(cfg.Spec.Sync.Paths, cfg.Spec.Workspace.MountPath)
50+
pairs, err := syncengine.ParsePairs(cfg.Spec.Sync.Paths, cfg.WorkspaceMountPath())
5151
if err != nil {
5252
return err
5353
}

internal/cli/up.go

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -55,19 +55,12 @@ func newUpCmd(opts *Options) *cobra.Command {
5555
ui.stepDone("namespace", ns)
5656
labels := labelsForSession(opts, cfg, sn)
5757
annotations := annotationsForSession(cfg)
58-
pvc := pvcName(cfg, sn)
59-
usePVC := usesWorkspacePVC(cfg)
58+
volumes := cfg.EffectiveVolumes()
6059
pod := podName(sn)
6160
if dryRun {
6261
ui.section("Dry Run")
6362
fmt.Fprintf(cmd.OutOrStdout(), "DRY RUN: session=%s namespace=%s\n", sn, ns)
64-
if usePVC && cfg.Spec.Workspace.PVC.ClaimName == "" {
65-
fmt.Fprintf(cmd.OutOrStdout(), "- would apply pvc/%s\n", pvc)
66-
} else if usePVC {
67-
fmt.Fprintf(cmd.OutOrStdout(), "- using existing pvc/%s\n", pvc)
68-
} else {
69-
fmt.Fprintln(cmd.OutOrStdout(), "- using ephemeral workspace volume (emptyDir)")
70-
}
63+
fmt.Fprintf(cmd.OutOrStdout(), "- using %d configured volume(s)\n", len(volumes))
7164
fmt.Fprintf(cmd.OutOrStdout(), "- would apply pod/%s\n", pod)
7265
fmt.Fprintf(cmd.OutOrStdout(), "- would wait for pod readiness (timeout=%s)\n", waitTimeout)
7366
fmt.Fprintln(cmd.OutOrStdout(), "- would setup SSH config + managed SSH/port-forwards")
@@ -85,30 +78,14 @@ func newUpCmd(opts *Options) *cobra.Command {
8578
}
8679
ctx, cancel := defaultContext()
8780
defer cancel()
88-
89-
if usePVC && cfg.Spec.Workspace.PVC.ClaimName == "" {
90-
ui.stepRun("pvc", pvc)
91-
pvcManifest, err := kube.BuildPVCManifest(ns, pvc, cfg.Spec.Workspace.PVC.Size, cfg.Spec.Workspace.PVC.StorageClassName, labels, annotations)
92-
if err != nil {
93-
return err
94-
}
95-
if err := k.Apply(ctx, ns, pvcManifest); err != nil {
96-
return err
97-
}
98-
ui.stepDone("pvc", "applied")
99-
} else if usePVC {
100-
ui.stepDone("pvc", "using "+cfg.Spec.Workspace.PVC.ClaimName)
101-
} else {
102-
pvc = ""
103-
ui.stepDone("pvc", "disabled (workspace uses emptyDir)")
104-
}
81+
ui.stepDone("pvc", "not managed (use pre-created PVCs in spec.volumes)")
10582

10683
enableTmux := tmux || cfg.Spec.SSH.PersistentSessionEnabled()
10784
preStopCmd := resolvePreStopCommand(cfg, cfgPath)
10885
preparedSpec, err := kube.PreparePodSpec(
10986
cfg.Spec.PodTemplate.Spec,
110-
pvc,
111-
cfg.Spec.Workspace.MountPath,
87+
volumes,
88+
cfg.WorkspaceMountPath(),
11289
cfg.Spec.Sidecar.Image,
11390
enableTmux,
11491
preStopCmd,

internal/config/config.go

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,17 @@ type Metadata struct {
3131
}
3232

3333
type DevEnvSpec struct {
34-
Namespace string `yaml:"namespace"`
35-
KubeContext string `yaml:"kubeContext"`
36-
Session SessionSpec `yaml:"session"`
37-
Workspace Workspace `yaml:"workspace"`
38-
Sync SyncSpec `yaml:"sync"`
39-
Ports []PortMapping `yaml:"ports"`
40-
SSH SSHSpec `yaml:"ssh"`
41-
Lifecycle LifecycleSpec `yaml:"lifecycle"`
42-
Sidecar SidecarSpec `yaml:"sidecar"`
43-
PodTemplate PodTemplateRef `yaml:"podTemplate"`
34+
Namespace string `yaml:"namespace"`
35+
KubeContext string `yaml:"kubeContext"`
36+
Session SessionSpec `yaml:"session"`
37+
Volumes []corev1.Volume `yaml:"volumes"`
38+
Workspace *LegacyWorkspace `yaml:"workspace,omitempty"`
39+
Sync SyncSpec `yaml:"sync"`
40+
Ports []PortMapping `yaml:"ports"`
41+
SSH SSHSpec `yaml:"ssh"`
42+
Lifecycle LifecycleSpec `yaml:"lifecycle"`
43+
Sidecar SidecarSpec `yaml:"sidecar"`
44+
PodTemplate PodTemplateRef `yaml:"podTemplate"`
4445
}
4546

4647
type SidecarSpec struct {
@@ -54,15 +55,15 @@ type SessionSpec struct {
5455
Shareable bool `yaml:"shareable"`
5556
}
5657

57-
type Workspace struct {
58-
MountPath string `yaml:"mountPath"`
59-
PVC PVCSettings `yaml:"pvc"`
60-
}
58+
const (
59+
DefaultWorkspacePath = "/workspace"
60+
DefaultWorkspaceName = "workspace"
61+
)
6162

62-
type PVCSettings struct {
63-
ClaimName string `yaml:"claimName"`
64-
Size string `yaml:"size"`
65-
StorageClassName string `yaml:"storageClassName"`
63+
// LegacyWorkspace exists only to produce a clear migration error for removed config.
64+
type LegacyWorkspace struct {
65+
MountPath string `yaml:"mountPath"`
66+
PVC map[string]string `yaml:"pvc"`
6667
}
6768

6869
type PodTemplateRef struct {
@@ -173,8 +174,8 @@ func (d *DevEnvironment) Validate() error {
173174
if d.Metadata.Name == "" {
174175
return errors.New("metadata.name is required")
175176
}
176-
if d.Spec.Workspace.MountPath == "" {
177-
return errors.New("spec.workspace.mountPath is required")
177+
if d.Spec.Workspace != nil {
178+
return errors.New("spec.workspace is removed; use spec.volumes (k8s Volume) and podTemplate.spec.containers[*].volumeMounts")
178179
}
179180
if d.Spec.Sync.Engine != "syncthing" {
180181
return fmt.Errorf("spec.sync.engine must be syncthing, got %q", d.Spec.Sync.Engine)
@@ -229,6 +230,40 @@ func (s SyncthingSpec) AutoInstallEnabled() bool {
229230
return *s.AutoInstall
230231
}
231232

233+
func (d *DevEnvironment) EffectiveVolumes() []corev1.Volume {
234+
out := make([]corev1.Volume, 0, len(d.Spec.Volumes)+1)
235+
hasWorkspace := false
236+
for _, v := range d.Spec.Volumes {
237+
if v.Name == DefaultWorkspaceName {
238+
hasWorkspace = true
239+
}
240+
out = append(out, v)
241+
}
242+
if !hasWorkspace {
243+
out = append(out, corev1.Volume{
244+
Name: DefaultWorkspaceName,
245+
VolumeSource: corev1.VolumeSource{
246+
EmptyDir: &corev1.EmptyDirVolumeSource{},
247+
},
248+
})
249+
}
250+
return out
251+
}
252+
253+
func (d *DevEnvironment) WorkspaceMountPath() string {
254+
for _, c := range d.Spec.PodTemplate.Spec.Containers {
255+
if c.Name != "dev" {
256+
continue
257+
}
258+
for _, vm := range c.VolumeMounts {
259+
if vm.Name == DefaultWorkspaceName && strings.TrimSpace(vm.MountPath) != "" {
260+
return vm.MountPath
261+
}
262+
}
263+
}
264+
return DefaultWorkspacePath
265+
}
266+
232267
func validateSyncPaths(paths []string) error {
233268
for _, p := range paths {
234269
parts := strings.Split(p, ":")

0 commit comments

Comments
 (0)