Skip to content

Commit 0a71790

Browse files
committed
feat(up): make up setup-only with managed ssh and sync
1 parent 2dff355 commit 0a71790

6 files changed

Lines changed: 62 additions & 65 deletions

File tree

docs/command-reference.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
- `okdev version`
1616
- `okdev init [--template basic|gpu|llm-stack] [--force]`
1717
- `okdev validate`
18-
- `okdev up [--no-attach] [--wait-timeout 3m] [--dry-run]`
19-
- attach is enabled by default; use `--no-attach` to skip shell + background integrations
20-
- in attach flow, `spec.ports` are applied via managed SSH `LocalForward` rules
18+
- `okdev up [--wait-timeout 3m] [--dry-run]`
19+
- prepares pod/workspace, SSH config, managed SSH+port-forwards, and background sync (when enabled), then exits
20+
- `spec.ports` are applied via managed SSH `LocalForward` rules
2121
- `okdev down [--delete-pvc] [--dry-run]`
2222
- `okdev status [--all] [--all-users]`
2323
- `okdev list [--all-namespaces] [--all-users]`

docs/okdev-design.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ Core flow:
7171
2. `okdev up` creates/resumes a dev session in a namespace.
7272
3. `okdev connect` opens terminal/SSH and optionally starts port forwards.
7373
4. `okdev sync` mirrors local repo <-> pod workspace.
74-
5. Developer can leave and later reconnect from another machine via `okdev up --attach`.
74+
5. Developer can leave and later reconnect from another machine via `okdev up`.
7575
6. `okdev down` stops session (or deletes, based on policy).
7676

7777
---
@@ -226,11 +226,10 @@ Ownership model:
226226
- scaffolds `.okdev.yaml` from template and detects language/runtime hints
227227
- supports `-c, --config` to generate a custom config filename/path
228228

229-
- `okdev up [--no-attach] [--name] [-c|--config]`
229+
- `okdev up [--name] [-c|--config]`
230230
- create or resume session
231231
- wait for Pod Ready (watch-based readiness events)
232-
- by default, attach shell and auto-start configured sync/port-forward/ssh tunnel
233-
- with `--no-attach`, skip shell and background integrations
232+
- setup sync/port-forward/ssh integration, then exit
234233
- supports `--dry-run` to preview operations without applying
235234
- supports explicit `--session <name>` for multiple concurrent sessions per repo
236235

@@ -282,7 +281,7 @@ Problem with local-only tools: state is tied to one machine.
282281
okdev approach:
283282
- Session identity is cluster-native (`namespace + env name + labels`).
284283
- Local machine stores only ephemeral client metadata.
285-
- Any machine with kube access and repo can run `okdev up --attach`.
284+
- Any machine with kube access and repo can run `okdev up`.
286285

287286
Ownership model:
288287
- Owner identity resolution: `--owner` flag -> `OKDEV_OWNER` -> local `USER`.

docs/okdev-implementation-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Definition of done:
4646
Deliverables:
4747
- `okdev connect` (interactive shell/command execution)
4848
- `okdev ports` with config-defined forwards
49-
- Cross-machine reattach (`okdev up --attach`)
49+
- Cross-machine reattach (`okdev up`)
5050
- Local active-session pointer in repo metadata
5151

5252
Definition of done:

docs/quickstart.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ For a named session:
5252
./bin/okdev sync --mode up
5353
./bin/okdev ports
5454

55-
# one-stop attach flow (shell + sync + app ports + ssh tunnel)
56-
./bin/okdev up --attach
55+
# one-step setup flow (sync + app ports + ssh tunnel), then exit
56+
./bin/okdev up
5757

5858
# continuous sync (syncthing)
5959
./bin/okdev sync
@@ -67,7 +67,7 @@ Default sidecar image tag follows the running `okdev` binary version (`ghcr.io/<
6767
Use `spec.sync.exclude` for local ignore patterns and `spec.sync.remoteExclude` for remote-only ignore patterns.
6868
For SSH, default mode is `spec.ssh.mode=dev-container` (your dev image runs sshd). Set `spec.ssh.mode=sidecar` to use `ghcr.io/<repo-owner>/okdev-sshd:<okdev-version>` instead.
6969
`okdev ssh` and `okdev up` manage `~/.ssh/config` entries as `okdev-<session>`.
70-
Configured `spec.ports` are written as SSH `LocalForward` rules and can be auto-started by `okdev up --attach`.
70+
Configured `spec.ports` are written as SSH `LocalForward` rules and auto-start with `okdev up`.
7171

7272
Preview-only mode (no cluster changes):
7373

internal/cli/ssh.go

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -138,19 +138,24 @@ func ensureSSHKeyOnPod(opts *Options, cfg *config.DevEnvironment, namespace, pod
138138
}
139139

140140
script := fmt.Sprintf("mkdir -p ~/.ssh && chmod 700 ~/.ssh && touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && (grep -qxF %s ~/.ssh/authorized_keys || echo %s >> ~/.ssh/authorized_keys)", syncengine.ShellEscape(pubKey), syncengine.ShellEscape(pubKey))
141-
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
142-
defer cancel()
143141
k := newKubeClient(opts)
144142
container := sshTargetContainer(cfg)
145-
if container == "" {
146-
_, err = k.ExecSh(ctx, namespace, pod, script)
147-
} else {
148-
_, err = k.ExecShInContainer(ctx, namespace, pod, container, script)
149-
}
150-
if err != nil {
151-
return fmt.Errorf("install ssh key in pod: %w", err)
143+
var lastErr error
144+
for i := 0; i < 3; i++ {
145+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
146+
if container == "" {
147+
_, err = k.ExecSh(ctx, namespace, pod, script)
148+
} else {
149+
_, err = k.ExecShInContainer(ctx, namespace, pod, container, script)
150+
}
151+
cancel()
152+
if err == nil {
153+
return nil
154+
}
155+
lastErr = err
156+
time.Sleep(time.Duration(i+1) * 500 * time.Millisecond)
152157
}
153-
return nil
158+
return fmt.Errorf("install ssh key in pod: %w", lastErr)
154159
}
155160

156161
func defaultSSHKeyPath(cfg *config.DevEnvironment) (string, error) {
@@ -162,7 +167,11 @@ func defaultSSHKeyPath(cfg *config.DevEnvironment) (string, error) {
162167
if err != nil {
163168
return "", err
164169
}
165-
return filepath.Join(home, ".ssh", "okdev_ed25519"), nil
170+
legacy := filepath.Join(home, ".ssh", "okdev_ed25519")
171+
if _, err := os.Stat(legacy); err == nil {
172+
return legacy, nil
173+
}
174+
return filepath.Join(home, ".okdev", "ssh", "id_ed25519"), nil
166175
}
167176

168177
func sshTargetContainer(cfg *config.DevEnvironment) string {

internal/cli/up.go

Lines changed: 31 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,13 @@ import (
1616
)
1717

1818
func newUpCmd(opts *Options) *cobra.Command {
19-
var attach bool
20-
var noAttach bool
2119
var waitTimeout time.Duration
2220
var dryRun bool
2321

2422
cmd := &cobra.Command{
2523
Use: "up",
2624
Short: "Create or resume a dev session",
2725
RunE: func(cmd *cobra.Command, args []string) error {
28-
if noAttach {
29-
attach = false
30-
}
3126
cfg, ns, err := loadConfigAndNamespace(opts)
3227
if err != nil {
3328
return err
@@ -50,9 +45,8 @@ func newUpCmd(opts *Options) *cobra.Command {
5045
}
5146
fmt.Fprintf(cmd.OutOrStdout(), "- would apply pod/%s\n", pod)
5247
fmt.Fprintf(cmd.OutOrStdout(), "- would wait for pod readiness (timeout=%s)\n", waitTimeout)
53-
if attach {
54-
fmt.Fprintln(cmd.OutOrStdout(), "- would attach shell and start background sync/ports/ssh")
55-
}
48+
fmt.Fprintln(cmd.OutOrStdout(), "- would setup SSH config + managed SSH/port-forwards")
49+
fmt.Fprintln(cmd.OutOrStdout(), "- would start background sync (when sync.engine=syncthing)")
5650
return nil
5751
}
5852
if err := ensureSessionOwnership(opts, k, ns, sn, true); err != nil {
@@ -113,56 +107,51 @@ func newUpCmd(opts *Options) *cobra.Command {
113107
}
114108

115109
fmt.Fprintf(cmd.OutOrStdout(), "Session ready: %s (namespace: %s)\n", sn, ns)
116-
if attach {
117-
stopMaintenance := startSessionMaintenanceWithClient(k, cfg, ns, sn, cmd.OutOrStdout(), true, true)
118-
defer stopMaintenance()
119-
120-
if cfg.Spec.SSH.LocalPort > 0 && cfg.Spec.SSH.RemotePort > 0 {
121-
keyPath, keyErr := defaultSSHKeyPath(cfg)
122-
if keyErr != nil {
123-
fmt.Fprintf(cmd.ErrOrStderr(), "warning: failed to resolve SSH key path: %v\n", keyErr)
110+
if cfg.Spec.SSH.LocalPort > 0 && cfg.Spec.SSH.RemotePort > 0 {
111+
keyPath, keyErr := defaultSSHKeyPath(cfg)
112+
if keyErr != nil {
113+
fmt.Fprintf(cmd.ErrOrStderr(), "warning: failed to resolve SSH key path: %v\n", keyErr)
114+
} else {
115+
if err := ensureSSHKeyOnPod(opts, cfg, ns, pod, keyPath); err != nil {
116+
fmt.Fprintf(cmd.ErrOrStderr(), "warning: failed to setup SSH key in pod: %v\n", err)
117+
}
118+
alias := sshHostAlias(sn)
119+
cfgPath, _ := config.ResolvePath(opts.ConfigPath)
120+
if cfgErr := ensureSSHConfigEntry(alias, sn, cfg.Spec.SSH.User, cfg.Spec.SSH.LocalPort, cfg.Spec.SSH.RemotePort, keyPath, cfgPath, cfg.Spec.Ports); cfgErr != nil {
121+
fmt.Fprintf(cmd.ErrOrStderr(), "warning: failed to update ~/.ssh/config: %v\n", cfgErr)
124122
} else {
125-
if err := ensureSSHKeyOnPod(opts, cfg, ns, pod, keyPath); err != nil {
126-
fmt.Fprintf(cmd.ErrOrStderr(), "warning: failed to setup SSH key in pod: %v\n", err)
123+
// Force-refresh managed master so forward rules are always current after `okdev up`.
124+
_ = stopManagedSSHForward(alias)
125+
if err := startManagedSSHForward(alias); err != nil {
126+
fmt.Fprintf(cmd.ErrOrStderr(), "warning: failed to start managed SSH/port-forwards: %v\n", err)
127127
} else {
128-
alias := sshHostAlias(sn)
129-
cfgPath, _ := config.ResolvePath(opts.ConfigPath)
130-
if cfgErr := ensureSSHConfigEntry(alias, sn, cfg.Spec.SSH.User, cfg.Spec.SSH.LocalPort, cfg.Spec.SSH.RemotePort, keyPath, cfgPath, cfg.Spec.Ports); cfgErr != nil {
131-
fmt.Fprintf(cmd.ErrOrStderr(), "warning: failed to update ~/.ssh/config: %v\n", cfgErr)
132-
} else if err := startManagedSSHForward(alias); err != nil {
133-
fmt.Fprintf(cmd.ErrOrStderr(), "warning: failed to start managed SSH/port-forwards: %v\n", err)
128+
if len(cfg.Spec.Ports) > 0 {
129+
fmt.Fprintf(cmd.OutOrStdout(), "SSH ready: ssh %s (managed forwards active)\n", alias)
134130
} else {
135-
if len(cfg.Spec.Ports) > 0 {
136-
fmt.Fprintf(cmd.OutOrStdout(), "Background SSH tunnel + port-forwards active via %s\n", alias)
137-
} else {
138-
fmt.Fprintf(cmd.OutOrStdout(), "Background SSH tunnel active: ssh %s\n", alias)
139-
}
131+
fmt.Fprintf(cmd.OutOrStdout(), "SSH ready: ssh %s\n", alias)
140132
}
141133
}
142134
}
143135
}
136+
}
144137

145-
if cfg.Spec.Sync.Engine == "" || cfg.Spec.Sync.Engine == "syncthing" {
146-
logPath, started, err := startDetachedSyncthingSync(opts, "bi", sn)
147-
if err != nil {
148-
fmt.Fprintf(cmd.ErrOrStderr(), "warning: failed to start syncthing background sync: %v\n", err)
138+
if cfg.Spec.Sync.Engine == "" || cfg.Spec.Sync.Engine == "syncthing" {
139+
logPath, started, err := startDetachedSyncthingSync(opts, "bi", sn)
140+
if err != nil {
141+
fmt.Fprintf(cmd.ErrOrStderr(), "warning: failed to start syncthing background sync: %v\n", err)
142+
} else {
143+
if started {
144+
fmt.Fprintf(cmd.OutOrStdout(), "Background syncthing sync active (mode=bi). Logs: %s\n", logPath)
149145
} else {
150-
if started {
151-
fmt.Fprintf(cmd.OutOrStdout(), "Background syncthing sync active (mode=bi). Logs: %s\n", logPath)
152-
} else {
153-
fmt.Fprintf(cmd.OutOrStdout(), "Background syncthing sync already running (mode=bi). Logs: %s\n", logPath)
154-
}
146+
fmt.Fprintf(cmd.OutOrStdout(), "Background syncthing sync already running (mode=bi). Logs: %s\n", logPath)
155147
}
156148
}
157-
158-
return runConnectWithClient(k, ns, sn, []string{"sh", "-lc", "command -v bash >/dev/null 2>&1 && exec bash || exec sh"}, true)
159149
}
150+
160151
return nil
161152
},
162153
}
163154

164-
cmd.Flags().BoolVar(&attach, "attach", true, "Attach shell after session is ready")
165-
cmd.Flags().BoolVar(&noAttach, "no-attach", false, "Do not attach shell/background integrations after session is ready")
166155
cmd.Flags().DurationVar(&waitTimeout, "wait-timeout", 3*time.Minute, "Wait timeout for pod readiness")
167156
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Preview actions without applying resources")
168157
return cmd

0 commit comments

Comments
 (0)