Skip to content

Commit 1281e00

Browse files
acmoreclaude
andcommitted
feat(cli): add --ssh-mode embedded flag to okdev up
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7a1eef1 commit 1281e00

3 files changed

Lines changed: 78 additions & 6 deletions

File tree

internal/cli/up.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@ func newUpCmd(opts *Options) *cobra.Command {
1919
var waitTimeout time.Duration
2020
var dryRun bool
2121
var tmux bool
22+
var sshMode string
2223

2324
cmd := &cobra.Command{
2425
Use: "up",
2526
Short: "Create or resume a dev session",
2627
RunE: func(cmd *cobra.Command, args []string) error {
28+
if sshMode != "sidecar" && sshMode != "embedded" {
29+
return fmt.Errorf("invalid --ssh-mode %q: must be \"sidecar\" or \"embedded\"", sshMode)
30+
}
2731
ui := newUpUI(cmd.OutOrStdout(), cmd.ErrOrStderr())
2832
// Keep legacy config-path announce quiet for this command; we render it in staged output.
2933
prevQuiet, hadQuiet := os.LookupEnv("OKDEV_QUIET_CONFIG_ANNOUNCE")
@@ -88,6 +92,7 @@ func newUpCmd(opts *Options) *cobra.Command {
8892
cfg.WorkspaceMountPath(),
8993
cfg.Spec.Sidecar.Image,
9094
enableTmux,
95+
sshMode == "embedded",
9196
preStopCmd,
9297
)
9398
if err != nil {
@@ -200,6 +205,7 @@ func newUpCmd(opts *Options) *cobra.Command {
200205
cmd.Flags().DurationVar(&waitTimeout, "wait-timeout", 3*time.Minute, "Wait timeout for pod readiness")
201206
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Preview actions without applying resources")
202207
cmd.Flags().BoolVar(&tmux, "tmux", false, "Enable tmux persistent shell sessions in the sidecar")
208+
cmd.Flags().StringVar(&sshMode, "ssh-mode", "sidecar", "SSH server mode: sidecar (default) or embedded")
203209
return cmd
204210
}
205211

internal/kube/podspec.go

Lines changed: 11 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, volumes []corev1.Volume, workspaceMountPath, sidecarImage string, tmux bool, preStop string) (corev1.PodSpec, error) {
13+
func PreparePodSpec(podSpec corev1.PodSpec, volumes []corev1.Volume, workspaceMountPath, sidecarImage string, tmux bool, embeddedSSH bool, preStop string) (corev1.PodSpec, error) {
1414
if strings.TrimSpace(sidecarImage) == "" {
1515
return corev1.PodSpec{}, fmt.Errorf("sidecar image cannot be empty")
1616
}
@@ -96,6 +96,16 @@ func PreparePodSpec(podSpec corev1.PodSpec, volumes []corev1.Volume, workspaceMo
9696
Value: "1",
9797
})
9898
}
99+
if embeddedSSH {
100+
sidecarContainer.Env = append(sidecarContainer.Env, corev1.EnvVar{
101+
Name: "OKDEV_SSH_MODE",
102+
Value: "embedded",
103+
})
104+
sidecarContainer.Ports = append(sidecarContainer.Ports, corev1.ContainerPort{
105+
ContainerPort: 2222,
106+
Name: "embedded-ssh",
107+
})
108+
}
99109
spec.Containers = append(spec.Containers, sidecarContainer)
100110
}
101111

internal/kube/podspec_test.go

Lines changed: 61 additions & 5 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{}, nil, "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, "")
10+
spec, err := PreparePodSpec(corev1.PodSpec{}, nil, "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, false, "")
1111
if err != nil {
1212
t.Fatal(err)
1313
}
@@ -17,7 +17,7 @@ func TestPreparePodSpecShareProcessNamespace(t *testing.T) {
1717
}
1818

1919
func TestPreparePodSpecWorkspaceDefaultsToEmptyDir(t *testing.T) {
20-
spec, err := PreparePodSpec(corev1.PodSpec{}, nil, "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, "")
20+
spec, err := PreparePodSpec(corev1.PodSpec{}, nil, "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, false, "")
2121
if err != nil {
2222
t.Fatal(err)
2323
}
@@ -43,7 +43,7 @@ func TestPreparePodSpecUsesConfiguredWorkspaceVolume(t *testing.T) {
4343
},
4444
},
4545
}
46-
spec, err := PreparePodSpec(corev1.PodSpec{}, volumes, "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, "")
46+
spec, err := PreparePodSpec(corev1.PodSpec{}, volumes, "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, false, "")
4747
if err != nil {
4848
t.Fatal(err)
4949
}
@@ -59,7 +59,7 @@ func TestPreparePodSpecUsesConfiguredWorkspaceVolume(t *testing.T) {
5959
}
6060

6161
func TestPreparePodSpecAddsWorkspaceMountOnDevAndSidecar(t *testing.T) {
62-
spec, err := PreparePodSpec(corev1.PodSpec{}, nil, "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, "")
62+
spec, err := PreparePodSpec(corev1.PodSpec{}, nil, "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, false, "")
6363
if err != nil {
6464
t.Fatal(err)
6565
}
@@ -89,7 +89,7 @@ func TestPreparePodSpecAddsWorkspaceMountOnDevAndSidecar(t *testing.T) {
8989
}
9090

9191
func TestPreparePodSpecErrorsOnEmptyImage(t *testing.T) {
92-
if _, err := PreparePodSpec(corev1.PodSpec{}, nil, "/workspace", "", false, ""); err == nil {
92+
if _, err := PreparePodSpec(corev1.PodSpec{}, nil, "/workspace", "", false, false, ""); err == nil {
9393
t.Fatal("expected error for empty sidecar image")
9494
}
9595
}
@@ -112,3 +112,59 @@ func TestSyncthingImagePullPolicy(t *testing.T) {
112112
}
113113
}
114114
}
115+
116+
func findContainer(containers []corev1.Container, name string) *corev1.Container {
117+
for i := range containers {
118+
if containers[i].Name == name {
119+
return &containers[i]
120+
}
121+
}
122+
return nil
123+
}
124+
125+
func TestPreparePodSpecEmbeddedSSHMode(t *testing.T) {
126+
t.Run("embeddedSSH=false", func(t *testing.T) {
127+
spec, err := PreparePodSpec(corev1.PodSpec{}, nil, "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, false, "")
128+
if err != nil {
129+
t.Fatal(err)
130+
}
131+
sc := findContainer(spec.Containers, "okdev-sidecar")
132+
if sc == nil {
133+
t.Fatal("sidecar container not found")
134+
}
135+
for _, env := range sc.Env {
136+
if env.Name == "OKDEV_SSH_MODE" {
137+
t.Fatal("expected no OKDEV_SSH_MODE env var when embeddedSSH=false")
138+
}
139+
}
140+
})
141+
142+
t.Run("embeddedSSH=true", func(t *testing.T) {
143+
spec, err := PreparePodSpec(corev1.PodSpec{}, nil, "/workspace", "ghcr.io/acmore/okdev-sidecar:edge", false, true, "")
144+
if err != nil {
145+
t.Fatal(err)
146+
}
147+
sc := findContainer(spec.Containers, "okdev-sidecar")
148+
if sc == nil {
149+
t.Fatal("sidecar container not found")
150+
}
151+
foundEnv := false
152+
for _, env := range sc.Env {
153+
if env.Name == "OKDEV_SSH_MODE" && env.Value == "embedded" {
154+
foundEnv = true
155+
}
156+
}
157+
if !foundEnv {
158+
t.Fatal("expected OKDEV_SSH_MODE=embedded env var when embeddedSSH=true")
159+
}
160+
foundPort := false
161+
for _, p := range sc.Ports {
162+
if p.ContainerPort == 2222 && p.Name == "embedded-ssh" {
163+
foundPort = true
164+
}
165+
}
166+
if !foundPort {
167+
t.Fatal("expected port 2222 named embedded-ssh when embeddedSSH=true")
168+
}
169+
})
170+
}

0 commit comments

Comments
 (0)