vm run --rm: ephemeral sandboxes
New `--rm` flag deletes the VM once the ssh session or `-- cmd` exits, making `vm run` one-shot. Exit code from command mode still propagates correctly. Semantics: - Create fails → no VM to delete, nothing to do. - SSH-wait timeout → VM intentionally kept alive so `vm logs <name>` shows why; the timeout error already pointed users at that. Even with --rm, this path skips delete — a wedged sshd is exactly when you want post-mortem access. - Session/command ends (any exit code, any reason) → VM is deleted via `vm.delete` RPC. Uses a fresh 10s context so Ctrl-C during the session doesn't abort the cleanup. New vmDeleteFunc seam at the top of banger.go alongside the other RPC seams. Two tests cover the happy path (session ends cleanly → delete fires with correct ref) and the skip-on-timeout path (ssh wait errors → delete does NOT fire). README updated with an ephemeral example and a note about the timeout-skip behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3aa64a63c1
commit
b33f24865c
3 changed files with 142 additions and 3 deletions
|
|
@ -1358,6 +1358,7 @@ func TestRunVMRunWorkspacePreparesAndAttaches(t *testing.T) {
|
|||
api.VMCreateParams{Name: "devbox"},
|
||||
&spec,
|
||||
nil,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("runVMRun: %v", err)
|
||||
|
|
@ -1450,6 +1451,7 @@ func TestVMRunPrintsPostCreateProgress(t *testing.T) {
|
|||
api.VMCreateParams{Name: "devbox"},
|
||||
&spec,
|
||||
nil,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("runVMRun: %v", err)
|
||||
|
|
@ -1541,6 +1543,7 @@ func TestRunVMRunWarnsWhenToolingHarnessStartFails(t *testing.T) {
|
|||
api.VMCreateParams{Name: "devbox"},
|
||||
&spec,
|
||||
nil,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("runVMRun: %v", err)
|
||||
|
|
@ -1604,6 +1607,7 @@ func TestRunVMRunBareModeSkipsWorkspaceAndTooling(t *testing.T) {
|
|||
api.VMCreateParams{Name: "bare"},
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("runVMRun: %v", err)
|
||||
|
|
@ -1616,6 +1620,108 @@ func TestRunVMRunBareModeSkipsWorkspaceAndTooling(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRunVMRunRMDeletesAfterSessionExits(t *testing.T) {
|
||||
origBegin := vmCreateBeginFunc
|
||||
origWaitForSSH := guestWaitForSSHFunc
|
||||
origSSHExec := sshExecFunc
|
||||
origHealth := vmHealthFunc
|
||||
origDelete := vmDeleteFunc
|
||||
t.Cleanup(func() {
|
||||
vmCreateBeginFunc = origBegin
|
||||
guestWaitForSSHFunc = origWaitForSSH
|
||||
sshExecFunc = origSSHExec
|
||||
vmHealthFunc = origHealth
|
||||
vmDeleteFunc = origDelete
|
||||
})
|
||||
|
||||
vm := model.VMRecord{
|
||||
ID: "vm-id", Name: "tmpbox",
|
||||
Runtime: model.VMRuntime{State: model.VMStateRunning, GuestIP: "172.16.0.2"},
|
||||
}
|
||||
vmCreateBeginFunc = func(context.Context, string, api.VMCreateParams) (api.VMCreateBeginResult, error) {
|
||||
return api.VMCreateBeginResult{Operation: api.VMCreateOperation{ID: "op-1", Stage: "ready", Done: true, Success: true, VM: &vm}}, nil
|
||||
}
|
||||
guestWaitForSSHFunc = func(context.Context, string, string, time.Duration) error { return nil }
|
||||
sshExecFunc = func(context.Context, io.Reader, io.Writer, io.Writer, []string) error { return nil }
|
||||
vmHealthFunc = func(context.Context, string, string) (api.VMHealthResult, error) {
|
||||
return api.VMHealthResult{Healthy: false}, nil
|
||||
}
|
||||
deletedRef := ""
|
||||
vmDeleteFunc = func(_ context.Context, _, idOrName string) error {
|
||||
deletedRef = idOrName
|
||||
return nil
|
||||
}
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
err := runVMRun(
|
||||
context.Background(),
|
||||
"/tmp/bangerd.sock",
|
||||
model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
|
||||
strings.NewReader(""),
|
||||
&stdout, &stderr,
|
||||
api.VMCreateParams{Name: "tmpbox"},
|
||||
nil,
|
||||
nil,
|
||||
true, // --rm
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("runVMRun: %v", err)
|
||||
}
|
||||
if deletedRef != "tmpbox" {
|
||||
t.Fatalf("deletedRef = %q, want tmpbox", deletedRef)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunVMRunRMSkipsDeleteOnSSHWaitTimeout(t *testing.T) {
|
||||
origBegin := vmCreateBeginFunc
|
||||
origWaitForSSH := guestWaitForSSHFunc
|
||||
origDelete := vmDeleteFunc
|
||||
origTimeout := vmRunSSHTimeout
|
||||
vmRunSSHTimeout = 50 * time.Millisecond
|
||||
t.Cleanup(func() {
|
||||
vmCreateBeginFunc = origBegin
|
||||
guestWaitForSSHFunc = origWaitForSSH
|
||||
vmDeleteFunc = origDelete
|
||||
vmRunSSHTimeout = origTimeout
|
||||
})
|
||||
|
||||
vm := model.VMRecord{
|
||||
ID: "vm-id", Name: "slowvm",
|
||||
Runtime: model.VMRuntime{State: model.VMStateRunning, GuestIP: "172.16.0.2"},
|
||||
}
|
||||
vmCreateBeginFunc = func(context.Context, string, api.VMCreateParams) (api.VMCreateBeginResult, error) {
|
||||
return api.VMCreateBeginResult{Operation: api.VMCreateOperation{ID: "op-1", Stage: "ready", Done: true, Success: true, VM: &vm}}, nil
|
||||
}
|
||||
guestWaitForSSHFunc = func(ctx context.Context, _, _ string, _ time.Duration) error {
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
deleteCalled := false
|
||||
vmDeleteFunc = func(context.Context, string, string) error {
|
||||
deleteCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
err := runVMRun(
|
||||
context.Background(),
|
||||
"/tmp/bangerd.sock",
|
||||
model.DaemonConfig{SSHKeyPath: "/tmp/id_ed25519"},
|
||||
strings.NewReader(""),
|
||||
&stdout, &stderr,
|
||||
api.VMCreateParams{Name: "slowvm"},
|
||||
nil,
|
||||
nil,
|
||||
true, // --rm
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("want timeout error")
|
||||
}
|
||||
if deleteCalled {
|
||||
t.Fatal("VM should NOT be deleted on ssh-wait timeout even with --rm (keep for debugging)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunVMRunSSHTimeoutReturnsActionableError(t *testing.T) {
|
||||
origBegin := vmCreateBeginFunc
|
||||
origWaitForSSH := guestWaitForSSHFunc
|
||||
|
|
@ -1651,6 +1757,7 @@ func TestRunVMRunSSHTimeoutReturnsActionableError(t *testing.T) {
|
|||
api.VMCreateParams{Name: "slowvm"},
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
)
|
||||
if err == nil {
|
||||
t.Fatal("want timeout error")
|
||||
|
|
@ -1708,6 +1815,7 @@ func TestRunVMRunCommandModePropagatesExitCode(t *testing.T) {
|
|||
api.VMCreateParams{Name: "cmdbox"},
|
||||
nil,
|
||||
[]string{"false"},
|
||||
false,
|
||||
)
|
||||
var exitErr ExitCodeError
|
||||
if !errors.As(err, &exitErr) || exitErr.Code != 7 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue