Skip to content

Commit d2d5fe5

Browse files
acmoreclaude
andcommitted
feat(kube): add preStop lifecycle hook to dev container
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5bf2ae9 commit d2d5fe5

3 files changed

Lines changed: 54 additions & 11 deletions

File tree

internal/cli/up.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ func newUpCmd(opts *Options) *cobra.Command {
7575
cfg.Spec.Workspace.MountPath,
7676
cfg.Spec.Sidecar.Image,
7777
enableTmux,
78+
"",
7879
)
7980
if err != nil {
8081
return err

internal/kube/podspec.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010

1111
var semverTagPattern = regexp.MustCompile(`^v?\d+\.\d+\.\d+([.-][0-9A-Za-z.-]+)?$`)
1212

13-
func PreparePodSpec(podSpec corev1.PodSpec, workspaceClaim, workspaceMountPath, sidecarImage string, tmux bool) (corev1.PodSpec, error) {
13+
func PreparePodSpec(podSpec corev1.PodSpec, workspaceClaim, workspaceMountPath, sidecarImage string, tmux bool, preStop string) (corev1.PodSpec, error) {
1414
if strings.TrimSpace(sidecarImage) == "" {
1515
return corev1.PodSpec{}, fmt.Errorf("sidecar image cannot be empty")
1616
}
@@ -48,6 +48,17 @@ func PreparePodSpec(podSpec corev1.PodSpec, workspaceClaim, workspaceMountPath,
4848
})
4949
}
5050

51+
if preStop != "" && len(spec.Containers) > 0 {
52+
if spec.Containers[0].Lifecycle == nil {
53+
spec.Containers[0].Lifecycle = &corev1.Lifecycle{}
54+
}
55+
spec.Containers[0].Lifecycle.PreStop = &corev1.LifecycleHandler{
56+
Exec: &corev1.ExecAction{
57+
Command: []string{"sh", "-c", preStop},
58+
},
59+
}
60+
}
61+
5162
if !hasContainer(spec.Containers, "okdev-sidecar") {
5263
privileged := true
5364
sidecarContainer := corev1.Container{

internal/kube/podspec_test.go

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
)
88

99
func TestPreparePodSpecShareProcessNamespace(t *testing.T) {
10-
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false)
10+
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, "")
1111
if err != nil {
1212
t.Fatal(err)
1313
}
@@ -17,7 +17,7 @@ func TestPreparePodSpecShareProcessNamespace(t *testing.T) {
1717
}
1818

1919
func TestPreparePodSpecSidecarName(t *testing.T) {
20-
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false)
20+
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, "")
2121
if err != nil {
2222
t.Fatal(err)
2323
}
@@ -27,7 +27,7 @@ func TestPreparePodSpecSidecarName(t *testing.T) {
2727
}
2828

2929
func TestPreparePodSpecSidecarPorts(t *testing.T) {
30-
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false)
30+
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, "")
3131
if err != nil {
3232
t.Fatal(err)
3333
}
@@ -52,7 +52,7 @@ func TestPreparePodSpecSidecarPorts(t *testing.T) {
5252
}
5353

5454
func TestPreparePodSpecSidecarVolumeMounts(t *testing.T) {
55-
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false)
55+
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, "")
5656
if err != nil {
5757
t.Fatal(err)
5858
}
@@ -80,7 +80,7 @@ func TestPreparePodSpecSidecarVolumeMounts(t *testing.T) {
8080
}
8181

8282
func TestPreparePodSpecSidecarPrivileged(t *testing.T) {
83-
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false)
83+
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, "")
8484
if err != nil {
8585
t.Fatal(err)
8686
}
@@ -97,7 +97,7 @@ func TestPreparePodSpecSidecarPrivileged(t *testing.T) {
9797
}
9898

9999
func TestPreparePodSpecContainerCount(t *testing.T) {
100-
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false)
100+
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, "")
101101
if err != nil {
102102
t.Fatal(err)
103103
}
@@ -108,7 +108,7 @@ func TestPreparePodSpecContainerCount(t *testing.T) {
108108

109109
func TestPreparePodSpecSidecarAlwaysAdded(t *testing.T) {
110110
// Even with syncthingEnabled=false, sidecar is still added.
111-
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false)
111+
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, "")
112112
if err != nil {
113113
t.Fatal(err)
114114
}
@@ -118,14 +118,14 @@ func TestPreparePodSpecSidecarAlwaysAdded(t *testing.T) {
118118
}
119119

120120
func TestPreparePodSpecErrorsOnEmptyImage(t *testing.T) {
121-
_, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "", false)
121+
_, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "", false, "")
122122
if err == nil {
123123
t.Fatal("expected error for empty sidecar image")
124124
}
125125
}
126126

127127
func TestPreparePodSpecTmuxEnvEnabled(t *testing.T) {
128-
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", true)
128+
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", true, "")
129129
if err != nil {
130130
t.Fatal(err)
131131
}
@@ -148,7 +148,7 @@ func TestPreparePodSpecTmuxEnvEnabled(t *testing.T) {
148148
}
149149

150150
func TestPreparePodSpecTmuxEnvDisabled(t *testing.T) {
151-
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false)
151+
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, "")
152152
if err != nil {
153153
t.Fatal(err)
154154
}
@@ -166,6 +166,37 @@ func TestPreparePodSpecTmuxEnvDisabled(t *testing.T) {
166166
}
167167
}
168168

169+
func TestPreparePodSpecPreStopHook(t *testing.T) {
170+
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, "make clean")
171+
if err != nil {
172+
t.Fatal(err)
173+
}
174+
dev := spec.Containers[0]
175+
if dev.Lifecycle == nil || dev.Lifecycle.PreStop == nil {
176+
t.Fatal("expected preStop lifecycle hook on dev container")
177+
}
178+
if dev.Lifecycle.PreStop.Exec == nil {
179+
t.Fatal("expected exec action in preStop hook")
180+
}
181+
want := []string{"sh", "-c", "make clean"}
182+
for i, w := range want {
183+
if i >= len(dev.Lifecycle.PreStop.Exec.Command) || dev.Lifecycle.PreStop.Exec.Command[i] != w {
184+
t.Fatalf("expected preStop command %v, got %v", want, dev.Lifecycle.PreStop.Exec.Command)
185+
}
186+
}
187+
}
188+
189+
func TestPreparePodSpecNoPreStopHook(t *testing.T) {
190+
spec, err := PreparePodSpec(corev1.PodSpec{}, "ws-pvc", "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, "")
191+
if err != nil {
192+
t.Fatal(err)
193+
}
194+
dev := spec.Containers[0]
195+
if dev.Lifecycle != nil && dev.Lifecycle.PreStop != nil {
196+
t.Fatal("expected no preStop lifecycle hook when preStop is empty")
197+
}
198+
}
199+
169200
func TestSyncthingImagePullPolicy(t *testing.T) {
170201
cases := []struct {
171202
image string

0 commit comments

Comments
 (0)