1
0
forked from wrenn/wrenn

fix: resolve bugs and DRY violations in sandbox manager and API handlers

- Fix createFromSnapshot discarding memoryMB param (balloon optimization was dead)
- Fix double dm-snapshot removal in Pause() cleanupPauseFailure path
- Fix DestroySandbox RPC mapping all errors to CodeNotFound
- Fix handleFailed event consumer missing pausing/resuming → error transitions
- Fix stream resource leak in StreamUpload on early-return paths
- Add envs/cwd fields to ExecRequest proto for foreground exec parity
- Extract createResources rollback helper to eliminate 4x duplicated teardown
- Remove unused chClient.ping method
- Add .mcp.json to gitignore

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 02:30:32 +06:00
parent 74f85ce4e9
commit 62bede5dae
10 changed files with 272 additions and 181 deletions

1
.gitignore vendored
View File

@ -55,3 +55,4 @@ internal/dashboard/static/*
.dual-graph/ .dual-graph/
# Added by code-review-graph # Added by code-review-graph
.code-review-graph/ .code-review-graph/
.mcp.json

View File

@ -119,6 +119,8 @@ func (h *execHandler) Exec(w http.ResponseWriter, r *http.Request) {
Cmd: req.Cmd, Cmd: req.Cmd,
Args: req.Args, Args: req.Args,
TimeoutSec: req.TimeoutSec, TimeoutSec: req.TimeoutSec,
Envs: req.Envs,
Cwd: req.Cwd,
})) }))
if err != nil { if err != nil {
status, code, msg := agentErrToHTTP(err) status, code, msg := agentErrToHTTP(err)

View File

@ -89,6 +89,12 @@ func (h *filesStreamHandler) StreamUpload(w http.ResponseWriter, r *http.Request
// Open client-streaming RPC to host agent. // Open client-streaming RPC to host agent.
stream := agent.WriteFileStream(ctx) stream := agent.WriteFileStream(ctx)
var streamClosed bool
defer func() {
if !streamClosed {
stream.CloseAndReceive()
}
}()
// Send metadata first. // Send metadata first.
if err := stream.Send(&pb.WriteFileStreamRequest{ if err := stream.Send(&pb.WriteFileStreamRequest{
@ -127,6 +133,7 @@ func (h *filesStreamHandler) StreamUpload(w http.ResponseWriter, r *http.Request
} }
// Close and receive response. // Close and receive response.
streamClosed = true
if _, err := stream.CloseAndReceive(); err != nil { if _, err := stream.CloseAndReceive(); err != nil {
status, code, msg := agentErrToHTTP(err) status, code, msg := agentErrToHTTP(err)
writeError(w, status, code, msg) writeError(w, status, code, msg)

View File

@ -210,16 +210,14 @@ func (c *SandboxEventConsumer) handleStopped(ctx context.Context, sandboxID pgty
// or the CP's background goroutine publishes a failure. Uses conditional update // or the CP's background goroutine publishes a failure. Uses conditional update
// to avoid clobbering concurrent operations. // to avoid clobbering concurrent operations.
func (c *SandboxEventConsumer) handleFailed(ctx context.Context, sandboxID pgtype.UUID) { func (c *SandboxEventConsumer) handleFailed(ctx context.Context, sandboxID pgtype.UUID) {
// Try running → error (VM crash pushed by host agent). // Try each possible pre-failure state until one matches.
if _, err := c.db.UpdateSandboxStatusIf(ctx, db.UpdateSandboxStatusIfParams{ for _, fromStatus := range []string{"running", "starting", "pausing", "resuming"} {
ID: sandboxID, Status: "running", Status_2: "error", if _, err := c.db.UpdateSandboxStatusIf(ctx, db.UpdateSandboxStatusIfParams{
}); err == nil { ID: sandboxID, Status: fromStatus, Status_2: "error",
return }); err == nil {
return
}
} }
// Try starting → error (create failed).
_, _ = c.db.UpdateSandboxStatusIf(ctx, db.UpdateSandboxStatusIfParams{
ID: sandboxID, Status: "starting", Status_2: "error",
})
} }
func (c *SandboxEventConsumer) handleAutoPaused(ctx context.Context, sandboxID pgtype.UUID, _ SandboxEvent) { func (c *SandboxEventConsumer) handleAutoPaused(ctx context.Context, sandboxID pgtype.UUID, _ SandboxEvent) {

View File

@ -78,15 +78,30 @@ type ExecResult struct {
ExitCode int32 ExitCode int32
} }
// ExecOpts holds optional parameters for Exec.
type ExecOpts struct {
Envs map[string]string
Cwd string
}
// Exec runs a command inside the sandbox and collects all stdout/stderr output. // Exec runs a command inside the sandbox and collects all stdout/stderr output.
// It blocks until the command completes. // It blocks until the command completes.
func (c *Client) Exec(ctx context.Context, cmd string, args ...string) (*ExecResult, error) { func (c *Client) Exec(ctx context.Context, cmd string, args []string, opts *ExecOpts) (*ExecResult, error) {
stdin := false stdin := false
proc := &envdpb.ProcessConfig{
Cmd: cmd,
Args: args,
}
if opts != nil {
if len(opts.Envs) > 0 {
proc.Envs = opts.Envs
}
if opts.Cwd != "" {
proc.Cwd = &opts.Cwd
}
}
req := connect.NewRequest(&envdpb.StartRequest{ req := connect.NewRequest(&envdpb.StartRequest{
Process: &envdpb.ProcessConfig{ Process: proc,
Cmd: cmd,
Args: args,
},
Stdin: &stdin, Stdin: &stdin,
}) })

View File

@ -20,6 +20,7 @@ import (
pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen" pb "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen"
"git.omukk.dev/wrenn/wrenn/proto/hostagent/gen/hostagentv1connect" "git.omukk.dev/wrenn/wrenn/proto/hostagent/gen/hostagentv1connect"
"git.omukk.dev/wrenn/wrenn/internal/envdclient"
"git.omukk.dev/wrenn/wrenn/internal/sandbox" "git.omukk.dev/wrenn/wrenn/internal/sandbox"
) )
@ -90,7 +91,10 @@ func (s *Server) DestroySandbox(
req *connect.Request[pb.DestroySandboxRequest], req *connect.Request[pb.DestroySandboxRequest],
) (*connect.Response[pb.DestroySandboxResponse], error) { ) (*connect.Response[pb.DestroySandboxResponse], error) {
if err := s.mgr.Destroy(ctx, req.Msg.SandboxId); err != nil { if err := s.mgr.Destroy(ctx, req.Msg.SandboxId); err != nil {
return nil, connect.NewError(connect.CodeNotFound, err) if strings.Contains(err.Error(), "not found") {
return nil, connect.NewError(connect.CodeNotFound, err)
}
return nil, connect.NewError(connect.CodeInternal, err)
} }
return connect.NewResponse(&pb.DestroySandboxResponse{}), nil return connect.NewResponse(&pb.DestroySandboxResponse{}), nil
} }
@ -216,7 +220,12 @@ func (s *Server) Exec(
execCtx, cancel := context.WithTimeout(ctx, timeout) execCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel() defer cancel()
result, err := s.mgr.Exec(execCtx, msg.SandboxId, msg.Cmd, msg.Args...) var opts *envdclient.ExecOpts
if len(msg.Envs) > 0 || msg.Cwd != "" {
opts = &envdclient.ExecOpts{Envs: msg.Envs, Cwd: msg.Cwd}
}
result, err := s.mgr.Exec(execCtx, msg.SandboxId, msg.Cmd, msg.Args, opts)
if err != nil { if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("exec: %w", err)) return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("exec: %w", err))
} }

View File

@ -201,24 +201,30 @@ func (m *Manager) Create(ctx context.Context, sandboxID string, teamID, template
return nil, fmt.Errorf("create dm-snapshot: %w", err) return nil, fmt.Errorf("create dm-snapshot: %w", err)
} }
res := &createResources{
sandboxID: sandboxID,
loops: m.loops,
loopImage: baseRootfs,
dmDevice: dmDev,
cowPath: cowPath,
slots: m.slots,
}
// Allocate network slot. // Allocate network slot.
slotIdx, err := m.slots.Allocate() slotIdx, err := m.slots.Allocate()
if err != nil { if err != nil {
warnErr("dm-snapshot remove error", sandboxID, devicemapper.RemoveSnapshot(context.Background(), dmDev)) res.rollback()
os.Remove(cowPath)
m.loops.Release(baseRootfs)
return nil, fmt.Errorf("allocate network slot: %w", err) return nil, fmt.Errorf("allocate network slot: %w", err)
} }
res.slotIdx = slotIdx
slot := network.NewSlot(slotIdx) slot := network.NewSlot(slotIdx)
// Set up network. // Set up network.
if err := network.CreateNetwork(slot); err != nil { if err := network.CreateNetwork(slot); err != nil {
m.slots.Release(slotIdx) res.rollback()
warnErr("dm-snapshot remove error", sandboxID, devicemapper.RemoveSnapshot(context.Background(), dmDev))
os.Remove(cowPath)
m.loops.Release(baseRootfs)
return nil, fmt.Errorf("create network: %w", err) return nil, fmt.Errorf("create network: %w", err)
} }
res.slot = slot
// Boot VM — CH gets the dm device path. // Boot VM — CH gets the dm device path.
vmCfg := vm.VMConfig{ vmCfg := vm.VMConfig{
@ -238,13 +244,10 @@ func (m *Manager) Create(ctx context.Context, sandboxID string, teamID, template
} }
if _, err := m.vm.Create(ctx, vmCfg); err != nil { if _, err := m.vm.Create(ctx, vmCfg); err != nil {
warnErr("network cleanup error", sandboxID, network.RemoveNetwork(slot)) res.rollback()
m.slots.Release(slotIdx)
warnErr("dm-snapshot remove error", sandboxID, devicemapper.RemoveSnapshot(context.Background(), dmDev))
os.Remove(cowPath)
m.loops.Release(baseRootfs)
return nil, fmt.Errorf("create VM: %w", err) return nil, fmt.Errorf("create VM: %w", err)
} }
res.vm = m.vm
// Wait for envd to be ready. // Wait for envd to be ready.
client := envdclient.New(slot.HostIP.String()) client := envdclient.New(slot.HostIP.String())
@ -252,12 +255,7 @@ func (m *Manager) Create(ctx context.Context, sandboxID string, teamID, template
defer waitCancel() defer waitCancel()
if err := client.WaitUntilReady(waitCtx); err != nil { if err := client.WaitUntilReady(waitCtx); err != nil {
warnErr("vm destroy error", sandboxID, m.vm.Destroy(context.Background(), sandboxID)) res.rollback()
warnErr("network cleanup error", sandboxID, network.RemoveNetwork(slot))
m.slots.Release(slotIdx)
warnErr("dm-snapshot remove error", sandboxID, devicemapper.RemoveSnapshot(context.Background(), dmDev))
os.Remove(cowPath)
m.loops.Release(baseRootfs)
return nil, fmt.Errorf("wait for envd: %w", err) return nil, fmt.Errorf("wait for envd: %w", err)
} }
@ -538,14 +536,16 @@ func (m *Manager) Pause(ctx context.Context, sandboxID string) error {
// ── Step 7: Remove dm-snapshot and save CoW ────────────────────── // ── Step 7: Remove dm-snapshot and save CoW ──────────────────────
if sb.dmDevice != nil { if sb.dmDevice != nil {
if err := devicemapper.RemoveSnapshot(ctx, sb.dmDevice); err != nil { dmDev := sb.dmDevice
sb.dmDevice = nil
if err := devicemapper.RemoveSnapshot(ctx, dmDev); err != nil {
m.cleanupPauseFailure(sb, sandboxID, pauseDir) m.cleanupPauseFailure(sb, sandboxID, pauseDir)
return fmt.Errorf("remove dm-snapshot: %w", err) return fmt.Errorf("remove dm-snapshot: %w", err)
} }
snapshotCow := snapshot.CowPath(pauseDir, "") snapshotCow := snapshot.CowPath(pauseDir, "")
if err := os.Rename(sb.dmDevice.CowPath, snapshotCow); err != nil { if err := os.Rename(dmDev.CowPath, snapshotCow); err != nil {
os.Remove(sb.dmDevice.CowPath) os.Remove(dmDev.CowPath)
m.cleanupPauseFailure(sb, sandboxID, pauseDir) m.cleanupPauseFailure(sb, sandboxID, pauseDir)
return fmt.Errorf("move cow file: %w", err) return fmt.Errorf("move cow file: %w", err)
} }
@ -616,38 +616,42 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int,
return nil, fmt.Errorf("move cow file: %w", err) return nil, fmt.Errorf("move cow file: %w", err)
} }
rollbackCow := func() {
if err := os.Rename(cowPath, savedCow); err != nil {
slog.Warn("failed to rollback cow file", "src", cowPath, "dst", savedCow, "error", err)
}
}
// Restore dm-snapshot from existing persistent CoW file. // Restore dm-snapshot from existing persistent CoW file.
dmName := "wrenn-" + sandboxID dmName := "wrenn-" + sandboxID
dmDev, err := devicemapper.RestoreSnapshot(ctx, dmName, originLoop, cowPath, originSize) dmDev, err := devicemapper.RestoreSnapshot(ctx, dmName, originLoop, cowPath, originSize)
if err != nil { if err != nil {
m.loops.Release(baseImagePath) m.loops.Release(baseImagePath)
rollbackCow() os.Rename(cowPath, savedCow)
return nil, fmt.Errorf("restore dm-snapshot: %w", err) return nil, fmt.Errorf("restore dm-snapshot: %w", err)
} }
res := &createResources{
sandboxID: sandboxID,
loops: m.loops,
loopImage: baseImagePath,
dmDevice: dmDev,
slots: m.slots,
rollCow: func() {
if err := os.Rename(cowPath, savedCow); err != nil {
slog.Warn("failed to rollback cow file", "src", cowPath, "dst", savedCow, "error", err)
}
},
}
// Allocate network slot. // Allocate network slot.
slotIdx, err := m.slots.Allocate() slotIdx, err := m.slots.Allocate()
if err != nil { if err != nil {
warnErr("dm-snapshot remove error", sandboxID, devicemapper.RemoveSnapshot(context.Background(), dmDev)) res.rollback()
rollbackCow()
m.loops.Release(baseImagePath)
return nil, fmt.Errorf("allocate network slot: %w", err) return nil, fmt.Errorf("allocate network slot: %w", err)
} }
res.slotIdx = slotIdx
slot := network.NewSlot(slotIdx) slot := network.NewSlot(slotIdx)
if err := network.CreateNetwork(slot); err != nil { if err := network.CreateNetwork(slot); err != nil {
m.slots.Release(slotIdx) res.rollback()
warnErr("dm-snapshot remove error", sandboxID, devicemapper.RemoveSnapshot(context.Background(), dmDev))
rollbackCow()
m.loops.Release(baseImagePath)
return nil, fmt.Errorf("create network: %w", err) return nil, fmt.Errorf("create network: %w", err)
} }
res.slot = slot
// Restore VM from CH snapshot. // Restore VM from CH snapshot.
vmCfg := vm.VMConfig{ vmCfg := vm.VMConfig{
@ -655,6 +659,7 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int,
TemplateID: meta.TemplateID, TemplateID: meta.TemplateID,
KernelPath: m.resolveKernelPath(kernelVersion), KernelPath: m.resolveKernelPath(kernelVersion),
RootfsPath: dmDev.DevicePath, RootfsPath: dmDev.DevicePath,
MemoryMB: meta.MemoryMB,
NetworkNamespace: slot.NamespaceID, NetworkNamespace: slot.NamespaceID,
TapDevice: slot.TapName, TapDevice: slot.TapName,
TapMAC: slot.TapMAC, TapMAC: slot.TapMAC,
@ -665,13 +670,10 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int,
} }
if _, err := m.vm.CreateFromSnapshot(ctx, vmCfg, pauseDir); err != nil { if _, err := m.vm.CreateFromSnapshot(ctx, vmCfg, pauseDir); err != nil {
warnErr("network cleanup error", sandboxID, network.RemoveNetwork(slot)) res.rollback()
m.slots.Release(slotIdx)
warnErr("dm-snapshot remove error", sandboxID, devicemapper.RemoveSnapshot(context.Background(), dmDev))
rollbackCow()
m.loops.Release(baseImagePath)
return nil, fmt.Errorf("restore VM from snapshot: %w", err) return nil, fmt.Errorf("restore VM from snapshot: %w", err)
} }
res.vm = m.vm
// Wait for envd to be ready. // Wait for envd to be ready.
client := envdclient.New(slot.HostIP.String()) client := envdclient.New(slot.HostIP.String())
@ -679,12 +681,7 @@ func (m *Manager) Resume(ctx context.Context, sandboxID string, timeoutSec int,
if err := client.WaitUntilReady(waitCtx); err != nil { if err := client.WaitUntilReady(waitCtx); err != nil {
waitCancel() waitCancel()
warnErr("vm destroy error", sandboxID, m.vm.Destroy(context.Background(), sandboxID)) res.rollback()
warnErr("network cleanup error", sandboxID, network.RemoveNetwork(slot))
m.slots.Release(slotIdx)
warnErr("dm-snapshot remove error", sandboxID, devicemapper.RemoveSnapshot(context.Background(), dmDev))
rollbackCow()
m.loops.Release(baseImagePath)
return nil, fmt.Errorf("wait for envd: %w", err) return nil, fmt.Errorf("wait for envd: %w", err)
} }
waitCancel() waitCancel()
@ -905,7 +902,7 @@ func (m *Manager) FlattenRootfs(ctx context.Context, sandboxID string, teamID, t
func() { func() {
syncCtx, cancel := context.WithTimeout(ctx, 10*time.Second) syncCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel() defer cancel()
if _, err := sb.client.Exec(syncCtx, "/bin/sync"); err != nil { if _, err := sb.client.Exec(syncCtx, "/bin/sync", nil, nil); err != nil {
slog.Warn("flatten: guest sync failed (non-fatal)", "id", sb.ID, "error", err) slog.Warn("flatten: guest sync failed (non-fatal)", "id", sb.ID, "error", err)
} }
}() }()
@ -1001,7 +998,7 @@ func (m *Manager) DeleteSnapshot(teamID, templateID pgtype.UUID) error {
// CH handles memory restore internally (with on-demand paging). // CH handles memory restore internally (with on-demand paging).
// The template's rootfs.ext4 is a flattened standalone image — we create a // The template's rootfs.ext4 is a flattened standalone image — we create a
// dm-snapshot on top of it just like a normal Create. // dm-snapshot on top of it just like a normal Create.
func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID string, teamID, templateID pgtype.UUID, vcpus, _, timeoutSec, diskSizeMB int) (*models.Sandbox, error) { func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID string, teamID, templateID pgtype.UUID, vcpus, memoryMB, timeoutSec, diskSizeMB int) (*models.Sandbox, error) {
tmplDir := layout.TemplateDir(m.cfg.WrennDir, teamID, templateID) tmplDir := layout.TemplateDir(m.cfg.WrennDir, teamID, templateID)
// Set up dm-snapshot on the template's flattened rootfs. // Set up dm-snapshot on the template's flattened rootfs.
@ -1026,23 +1023,29 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID string, team
return nil, fmt.Errorf("create dm-snapshot: %w", err) return nil, fmt.Errorf("create dm-snapshot: %w", err)
} }
res := &createResources{
sandboxID: sandboxID,
loops: m.loops,
loopImage: baseRootfs,
dmDevice: dmDev,
cowPath: cowPath,
slots: m.slots,
}
// Allocate network. // Allocate network.
slotIdx, err := m.slots.Allocate() slotIdx, err := m.slots.Allocate()
if err != nil { if err != nil {
warnErr("dm-snapshot remove error", sandboxID, devicemapper.RemoveSnapshot(context.Background(), dmDev)) res.rollback()
os.Remove(cowPath)
m.loops.Release(baseRootfs)
return nil, fmt.Errorf("allocate network slot: %w", err) return nil, fmt.Errorf("allocate network slot: %w", err)
} }
res.slotIdx = slotIdx
slot := network.NewSlot(slotIdx) slot := network.NewSlot(slotIdx)
if err := network.CreateNetwork(slot); err != nil { if err := network.CreateNetwork(slot); err != nil {
m.slots.Release(slotIdx) res.rollback()
warnErr("dm-snapshot remove error", sandboxID, devicemapper.RemoveSnapshot(context.Background(), dmDev))
os.Remove(cowPath)
m.loops.Release(baseRootfs)
return nil, fmt.Errorf("create network: %w", err) return nil, fmt.Errorf("create network: %w", err)
} }
res.slot = slot
// Restore VM from CH snapshot. // Restore VM from CH snapshot.
vmCfg := vm.VMConfig{ vmCfg := vm.VMConfig{
@ -1051,6 +1054,7 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID string, team
KernelPath: m.cfg.KernelPath, KernelPath: m.cfg.KernelPath,
RootfsPath: dmDev.DevicePath, RootfsPath: dmDev.DevicePath,
VCPUs: vcpus, VCPUs: vcpus,
MemoryMB: memoryMB,
NetworkNamespace: slot.NamespaceID, NetworkNamespace: slot.NamespaceID,
TapDevice: slot.TapName, TapDevice: slot.TapName,
TapMAC: slot.TapMAC, TapMAC: slot.TapMAC,
@ -1061,13 +1065,10 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID string, team
} }
if _, err := m.vm.CreateFromSnapshot(ctx, vmCfg, tmplDir); err != nil { if _, err := m.vm.CreateFromSnapshot(ctx, vmCfg, tmplDir); err != nil {
warnErr("network cleanup error", sandboxID, network.RemoveNetwork(slot)) res.rollback()
m.slots.Release(slotIdx)
warnErr("dm-snapshot remove error", sandboxID, devicemapper.RemoveSnapshot(context.Background(), dmDev))
os.Remove(cowPath)
m.loops.Release(baseRootfs)
return nil, fmt.Errorf("restore VM from snapshot: %w", err) return nil, fmt.Errorf("restore VM from snapshot: %w", err)
} }
res.vm = m.vm
// Wait for envd. // Wait for envd.
client := envdclient.New(slot.HostIP.String()) client := envdclient.New(slot.HostIP.String())
@ -1075,12 +1076,7 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID string, team
if err := client.WaitUntilReady(waitCtx); err != nil { if err := client.WaitUntilReady(waitCtx); err != nil {
waitCancel() waitCancel()
warnErr("vm destroy error", sandboxID, m.vm.Destroy(context.Background(), sandboxID)) res.rollback()
warnErr("network cleanup error", sandboxID, network.RemoveNetwork(slot))
m.slots.Release(slotIdx)
warnErr("dm-snapshot remove error", sandboxID, devicemapper.RemoveSnapshot(context.Background(), dmDev))
os.Remove(cowPath)
m.loops.Release(baseRootfs)
return nil, fmt.Errorf("wait for envd: %w", err) return nil, fmt.Errorf("wait for envd: %w", err)
} }
waitCancel() waitCancel()
@ -1109,6 +1105,7 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID string, team
TemplateTeamID: teamID.Bytes, TemplateTeamID: teamID.Bytes,
TemplateID: templateID.Bytes, TemplateID: templateID.Bytes,
VCPUs: vcpus, VCPUs: vcpus,
MemoryMB: memoryMB,
TimeoutSec: timeoutSec, TimeoutSec: timeoutSec,
SlotIndex: slotIdx, SlotIndex: slotIdx,
HostIP: slot.HostIP, HostIP: slot.HostIP,
@ -1143,7 +1140,7 @@ func (m *Manager) createFromSnapshot(ctx context.Context, sandboxID string, team
} }
// Exec runs a command inside a sandbox. // Exec runs a command inside a sandbox.
func (m *Manager) Exec(ctx context.Context, sandboxID string, cmd string, args ...string) (*envdclient.ExecResult, error) { func (m *Manager) Exec(ctx context.Context, sandboxID string, cmd string, args []string, opts *envdclient.ExecOpts) (*envdclient.ExecResult, error) {
sb, err := m.get(sandboxID) sb, err := m.get(sandboxID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -1157,7 +1154,7 @@ func (m *Manager) Exec(ctx context.Context, sandboxID string, cmd string, args .
sb.LastActiveAt = time.Now() sb.LastActiveAt = time.Now()
m.mu.Unlock() m.mu.Unlock()
return sb.client.Exec(ctx, cmd, args...) return sb.client.Exec(ctx, cmd, args, opts)
} }
// ExecStream runs a command inside a sandbox and returns a channel of streaming events. // ExecStream runs a command inside a sandbox and returns a channel of streaming events.
@ -1538,6 +1535,44 @@ func warnErr(msg string, id string, err error) {
} }
} }
// createResources tracks partially-acquired resources during sandbox creation
// so they can be rolled back in reverse order on failure.
type createResources struct {
sandboxID string
loops *devicemapper.LoopRegistry
vm *vm.Manager
loopImage string
dmDevice *devicemapper.SnapshotDevice
cowPath string
slotIdx int
slots *network.SlotAllocator
slot *network.Slot
rollCow func() // optional custom cow rollback (e.g. rename back)
}
func (r *createResources) rollback() {
if r.vm != nil && r.sandboxID != "" {
warnErr("vm destroy error", r.sandboxID, r.vm.Destroy(context.Background(), r.sandboxID))
}
if r.slot != nil {
warnErr("network cleanup error", r.sandboxID, network.RemoveNetwork(r.slot))
}
if r.slots != nil && r.slotIdx > 0 {
r.slots.Release(r.slotIdx)
}
if r.dmDevice != nil {
warnErr("dm-snapshot remove error", r.sandboxID, devicemapper.RemoveSnapshot(context.Background(), r.dmDevice))
}
if r.rollCow != nil {
r.rollCow()
} else if r.cowPath != "" {
os.Remove(r.cowPath)
}
if r.loopImage != "" {
r.loops.Release(r.loopImage)
}
}
// startCrashWatcher monitors the VM process for unexpected exits. // startCrashWatcher monitors the VM process for unexpected exits.
// If the process exits while the sandbox is still in m.boxes (i.e. not a // If the process exits while the sandbox is still in m.boxes (i.e. not a
// deliberate Destroy), the sandbox is cleaned up and a sandbox.error event // deliberate Destroy), the sandbox is cleaned up and a sandbox.error event

View File

@ -206,8 +206,3 @@ func (c *chClient) resizeBalloon(ctx context.Context, sizeBytes int64) error {
"desired_balloon": sizeBytes, "desired_balloon": sizeBytes,
}) })
} }
// ping checks if the VMM is alive and ready to accept commands.
func (c *chClient) ping(ctx context.Context) error {
return c.do(ctx, http.MethodGet, "/api/v1/vmm.ping", nil)
}

View File

@ -759,7 +759,11 @@ type ExecRequest struct {
Cmd string `protobuf:"bytes,2,opt,name=cmd,proto3" json:"cmd,omitempty"` Cmd string `protobuf:"bytes,2,opt,name=cmd,proto3" json:"cmd,omitempty"`
Args []string `protobuf:"bytes,3,rep,name=args,proto3" json:"args,omitempty"` Args []string `protobuf:"bytes,3,rep,name=args,proto3" json:"args,omitempty"`
// Timeout for the command in seconds (default: 30). // Timeout for the command in seconds (default: 30).
TimeoutSec int32 `protobuf:"varint,4,opt,name=timeout_sec,json=timeoutSec,proto3" json:"timeout_sec,omitempty"` TimeoutSec int32 `protobuf:"varint,4,opt,name=timeout_sec,json=timeoutSec,proto3" json:"timeout_sec,omitempty"`
// Environment variables to set for the command.
Envs map[string]string `protobuf:"bytes,5,rep,name=envs,proto3" json:"envs,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
// Working directory for the command.
Cwd string `protobuf:"bytes,6,opt,name=cwd,proto3" json:"cwd,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -822,6 +826,20 @@ func (x *ExecRequest) GetTimeoutSec() int32 {
return 0 return 0
} }
func (x *ExecRequest) GetEnvs() map[string]string {
if x != nil {
return x.Envs
}
return nil
}
func (x *ExecRequest) GetCwd() string {
if x != nil {
return x.Cwd
}
return ""
}
type ExecResponse struct { type ExecResponse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Stdout []byte `protobuf:"bytes,1,opt,name=stdout,proto3" json:"stdout,omitempty"` Stdout []byte `protobuf:"bytes,1,opt,name=stdout,proto3" json:"stdout,omitempty"`
@ -4196,14 +4214,19 @@ const file_hostagent_proto_rawDesc = "" +
"\ateam_id\x18\x02 \x01(\tR\x06teamId\x12\x1f\n" + "\ateam_id\x18\x02 \x01(\tR\x06teamId\x12\x1f\n" +
"\vtemplate_id\x18\x03 \x01(\tR\n" + "\vtemplate_id\x18\x03 \x01(\tR\n" +
"templateId\"\x18\n" + "templateId\"\x18\n" +
"\x16DeleteSnapshotResponse\"s\n" + "\x16DeleteSnapshotResponse\"\xf7\x01\n" +
"\vExecRequest\x12\x1d\n" + "\vExecRequest\x12\x1d\n" +
"\n" + "\n" +
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x10\n" + "sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x10\n" +
"\x03cmd\x18\x02 \x01(\tR\x03cmd\x12\x12\n" + "\x03cmd\x18\x02 \x01(\tR\x03cmd\x12\x12\n" +
"\x04args\x18\x03 \x03(\tR\x04args\x12\x1f\n" + "\x04args\x18\x03 \x03(\tR\x04args\x12\x1f\n" +
"\vtimeout_sec\x18\x04 \x01(\x05R\n" + "\vtimeout_sec\x18\x04 \x01(\x05R\n" +
"timeoutSec\"[\n" + "timeoutSec\x127\n" +
"\x04envs\x18\x05 \x03(\v2#.hostagent.v1.ExecRequest.EnvsEntryR\x04envs\x12\x10\n" +
"\x03cwd\x18\x06 \x01(\tR\x03cwd\x1a7\n" +
"\tEnvsEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"[\n" +
"\fExecResponse\x12\x16\n" + "\fExecResponse\x12\x16\n" +
"\x06stdout\x18\x01 \x01(\fR\x06stdout\x12\x16\n" + "\x06stdout\x18\x01 \x01(\fR\x06stdout\x12\x16\n" +
"\x06stderr\x18\x02 \x01(\fR\x06stderr\x12\x1b\n" + "\x06stderr\x18\x02 \x01(\fR\x06stderr\x12\x1b\n" +
@ -4486,7 +4509,7 @@ func file_hostagent_proto_rawDescGZIP() []byte {
return file_hostagent_proto_rawDescData return file_hostagent_proto_rawDescData
} }
var file_hostagent_proto_msgTypes = make([]protoimpl.MessageInfo, 76) var file_hostagent_proto_msgTypes = make([]protoimpl.MessageInfo, 77)
var file_hostagent_proto_goTypes = []any{ var file_hostagent_proto_goTypes = []any{
(*CreateSandboxRequest)(nil), // 0: hostagent.v1.CreateSandboxRequest (*CreateSandboxRequest)(nil), // 0: hostagent.v1.CreateSandboxRequest
(*CreateSandboxResponse)(nil), // 1: hostagent.v1.CreateSandboxResponse (*CreateSandboxResponse)(nil), // 1: hostagent.v1.CreateSandboxResponse
@ -4561,99 +4584,101 @@ var file_hostagent_proto_goTypes = []any{
nil, // 70: hostagent.v1.CreateSandboxResponse.MetadataEntry nil, // 70: hostagent.v1.CreateSandboxResponse.MetadataEntry
nil, // 71: hostagent.v1.ResumeSandboxRequest.DefaultEnvEntry nil, // 71: hostagent.v1.ResumeSandboxRequest.DefaultEnvEntry
nil, // 72: hostagent.v1.ResumeSandboxResponse.MetadataEntry nil, // 72: hostagent.v1.ResumeSandboxResponse.MetadataEntry
nil, // 73: hostagent.v1.SandboxInfo.MetadataEntry nil, // 73: hostagent.v1.ExecRequest.EnvsEntry
nil, // 74: hostagent.v1.PtyAttachRequest.EnvsEntry nil, // 74: hostagent.v1.SandboxInfo.MetadataEntry
nil, // 75: hostagent.v1.StartBackgroundRequest.EnvsEntry nil, // 75: hostagent.v1.PtyAttachRequest.EnvsEntry
nil, // 76: hostagent.v1.StartBackgroundRequest.EnvsEntry
} }
var file_hostagent_proto_depIdxs = []int32{ var file_hostagent_proto_depIdxs = []int32{
69, // 0: hostagent.v1.CreateSandboxRequest.default_env:type_name -> hostagent.v1.CreateSandboxRequest.DefaultEnvEntry 69, // 0: hostagent.v1.CreateSandboxRequest.default_env:type_name -> hostagent.v1.CreateSandboxRequest.DefaultEnvEntry
70, // 1: hostagent.v1.CreateSandboxResponse.metadata:type_name -> hostagent.v1.CreateSandboxResponse.MetadataEntry 70, // 1: hostagent.v1.CreateSandboxResponse.metadata:type_name -> hostagent.v1.CreateSandboxResponse.MetadataEntry
71, // 2: hostagent.v1.ResumeSandboxRequest.default_env:type_name -> hostagent.v1.ResumeSandboxRequest.DefaultEnvEntry 71, // 2: hostagent.v1.ResumeSandboxRequest.default_env:type_name -> hostagent.v1.ResumeSandboxRequest.DefaultEnvEntry
72, // 3: hostagent.v1.ResumeSandboxResponse.metadata:type_name -> hostagent.v1.ResumeSandboxResponse.MetadataEntry 72, // 3: hostagent.v1.ResumeSandboxResponse.metadata:type_name -> hostagent.v1.ResumeSandboxResponse.MetadataEntry
16, // 4: hostagent.v1.ListSandboxesResponse.sandboxes:type_name -> hostagent.v1.SandboxInfo 73, // 4: hostagent.v1.ExecRequest.envs:type_name -> hostagent.v1.ExecRequest.EnvsEntry
73, // 5: hostagent.v1.SandboxInfo.metadata:type_name -> hostagent.v1.SandboxInfo.MetadataEntry 16, // 5: hostagent.v1.ListSandboxesResponse.sandboxes:type_name -> hostagent.v1.SandboxInfo
23, // 6: hostagent.v1.ExecStreamResponse.start:type_name -> hostagent.v1.ExecStreamStart 74, // 6: hostagent.v1.SandboxInfo.metadata:type_name -> hostagent.v1.SandboxInfo.MetadataEntry
24, // 7: hostagent.v1.ExecStreamResponse.data:type_name -> hostagent.v1.ExecStreamData 23, // 7: hostagent.v1.ExecStreamResponse.start:type_name -> hostagent.v1.ExecStreamStart
25, // 8: hostagent.v1.ExecStreamResponse.end:type_name -> hostagent.v1.ExecStreamEnd 24, // 8: hostagent.v1.ExecStreamResponse.data:type_name -> hostagent.v1.ExecStreamData
27, // 9: hostagent.v1.WriteFileStreamRequest.meta:type_name -> hostagent.v1.WriteFileStreamMeta 25, // 9: hostagent.v1.ExecStreamResponse.end:type_name -> hostagent.v1.ExecStreamEnd
33, // 10: hostagent.v1.ListDirResponse.entries:type_name -> hostagent.v1.FileEntry 27, // 10: hostagent.v1.WriteFileStreamRequest.meta:type_name -> hostagent.v1.WriteFileStreamMeta
33, // 11: hostagent.v1.MakeDirResponse.entry:type_name -> hostagent.v1.FileEntry 33, // 11: hostagent.v1.ListDirResponse.entries:type_name -> hostagent.v1.FileEntry
42, // 12: hostagent.v1.GetSandboxMetricsResponse.points:type_name -> hostagent.v1.MetricPoint 33, // 12: hostagent.v1.MakeDirResponse.entry:type_name -> hostagent.v1.FileEntry
42, // 13: hostagent.v1.FlushSandboxMetricsResponse.points_10m:type_name -> hostagent.v1.MetricPoint 42, // 13: hostagent.v1.GetSandboxMetricsResponse.points:type_name -> hostagent.v1.MetricPoint
42, // 14: hostagent.v1.FlushSandboxMetricsResponse.points_2h:type_name -> hostagent.v1.MetricPoint 42, // 14: hostagent.v1.FlushSandboxMetricsResponse.points_10m:type_name -> hostagent.v1.MetricPoint
42, // 15: hostagent.v1.FlushSandboxMetricsResponse.points_24h:type_name -> hostagent.v1.MetricPoint 42, // 15: hostagent.v1.FlushSandboxMetricsResponse.points_2h:type_name -> hostagent.v1.MetricPoint
74, // 16: hostagent.v1.PtyAttachRequest.envs:type_name -> hostagent.v1.PtyAttachRequest.EnvsEntry 42, // 16: hostagent.v1.FlushSandboxMetricsResponse.points_24h:type_name -> hostagent.v1.MetricPoint
51, // 17: hostagent.v1.PtyAttachResponse.started:type_name -> hostagent.v1.PtyStarted 75, // 17: hostagent.v1.PtyAttachRequest.envs:type_name -> hostagent.v1.PtyAttachRequest.EnvsEntry
52, // 18: hostagent.v1.PtyAttachResponse.output:type_name -> hostagent.v1.PtyOutput 51, // 18: hostagent.v1.PtyAttachResponse.started:type_name -> hostagent.v1.PtyStarted
53, // 19: hostagent.v1.PtyAttachResponse.exited:type_name -> hostagent.v1.PtyExited 52, // 19: hostagent.v1.PtyAttachResponse.output:type_name -> hostagent.v1.PtyOutput
75, // 20: hostagent.v1.StartBackgroundRequest.envs:type_name -> hostagent.v1.StartBackgroundRequest.EnvsEntry 53, // 20: hostagent.v1.PtyAttachResponse.exited:type_name -> hostagent.v1.PtyExited
63, // 21: hostagent.v1.ListProcessesResponse.processes:type_name -> hostagent.v1.ProcessEntry 76, // 21: hostagent.v1.StartBackgroundRequest.envs:type_name -> hostagent.v1.StartBackgroundRequest.EnvsEntry
23, // 22: hostagent.v1.ConnectProcessResponse.start:type_name -> hostagent.v1.ExecStreamStart 63, // 22: hostagent.v1.ListProcessesResponse.processes:type_name -> hostagent.v1.ProcessEntry
24, // 23: hostagent.v1.ConnectProcessResponse.data:type_name -> hostagent.v1.ExecStreamData 23, // 23: hostagent.v1.ConnectProcessResponse.start:type_name -> hostagent.v1.ExecStreamStart
25, // 24: hostagent.v1.ConnectProcessResponse.end:type_name -> hostagent.v1.ExecStreamEnd 24, // 24: hostagent.v1.ConnectProcessResponse.data:type_name -> hostagent.v1.ExecStreamData
0, // 25: hostagent.v1.HostAgentService.CreateSandbox:input_type -> hostagent.v1.CreateSandboxRequest 25, // 25: hostagent.v1.ConnectProcessResponse.end:type_name -> hostagent.v1.ExecStreamEnd
2, // 26: hostagent.v1.HostAgentService.DestroySandbox:input_type -> hostagent.v1.DestroySandboxRequest 0, // 26: hostagent.v1.HostAgentService.CreateSandbox:input_type -> hostagent.v1.CreateSandboxRequest
4, // 27: hostagent.v1.HostAgentService.PauseSandbox:input_type -> hostagent.v1.PauseSandboxRequest 2, // 27: hostagent.v1.HostAgentService.DestroySandbox:input_type -> hostagent.v1.DestroySandboxRequest
6, // 28: hostagent.v1.HostAgentService.ResumeSandbox:input_type -> hostagent.v1.ResumeSandboxRequest 4, // 28: hostagent.v1.HostAgentService.PauseSandbox:input_type -> hostagent.v1.PauseSandboxRequest
12, // 29: hostagent.v1.HostAgentService.Exec:input_type -> hostagent.v1.ExecRequest 6, // 29: hostagent.v1.HostAgentService.ResumeSandbox:input_type -> hostagent.v1.ResumeSandboxRequest
14, // 30: hostagent.v1.HostAgentService.ListSandboxes:input_type -> hostagent.v1.ListSandboxesRequest 12, // 30: hostagent.v1.HostAgentService.Exec:input_type -> hostagent.v1.ExecRequest
17, // 31: hostagent.v1.HostAgentService.WriteFile:input_type -> hostagent.v1.WriteFileRequest 14, // 31: hostagent.v1.HostAgentService.ListSandboxes:input_type -> hostagent.v1.ListSandboxesRequest
19, // 32: hostagent.v1.HostAgentService.ReadFile:input_type -> hostagent.v1.ReadFileRequest 17, // 32: hostagent.v1.HostAgentService.WriteFile:input_type -> hostagent.v1.WriteFileRequest
31, // 33: hostagent.v1.HostAgentService.ListDir:input_type -> hostagent.v1.ListDirRequest 19, // 33: hostagent.v1.HostAgentService.ReadFile:input_type -> hostagent.v1.ReadFileRequest
34, // 34: hostagent.v1.HostAgentService.MakeDir:input_type -> hostagent.v1.MakeDirRequest 31, // 34: hostagent.v1.HostAgentService.ListDir:input_type -> hostagent.v1.ListDirRequest
36, // 35: hostagent.v1.HostAgentService.RemovePath:input_type -> hostagent.v1.RemovePathRequest 34, // 35: hostagent.v1.HostAgentService.MakeDir:input_type -> hostagent.v1.MakeDirRequest
8, // 36: hostagent.v1.HostAgentService.CreateSnapshot:input_type -> hostagent.v1.CreateSnapshotRequest 36, // 36: hostagent.v1.HostAgentService.RemovePath:input_type -> hostagent.v1.RemovePathRequest
10, // 37: hostagent.v1.HostAgentService.DeleteSnapshot:input_type -> hostagent.v1.DeleteSnapshotRequest 8, // 37: hostagent.v1.HostAgentService.CreateSnapshot:input_type -> hostagent.v1.CreateSnapshotRequest
21, // 38: hostagent.v1.HostAgentService.ExecStream:input_type -> hostagent.v1.ExecStreamRequest 10, // 38: hostagent.v1.HostAgentService.DeleteSnapshot:input_type -> hostagent.v1.DeleteSnapshotRequest
26, // 39: hostagent.v1.HostAgentService.WriteFileStream:input_type -> hostagent.v1.WriteFileStreamRequest 21, // 39: hostagent.v1.HostAgentService.ExecStream:input_type -> hostagent.v1.ExecStreamRequest
29, // 40: hostagent.v1.HostAgentService.ReadFileStream:input_type -> hostagent.v1.ReadFileStreamRequest 26, // 40: hostagent.v1.HostAgentService.WriteFileStream:input_type -> hostagent.v1.WriteFileStreamRequest
38, // 41: hostagent.v1.HostAgentService.PingSandbox:input_type -> hostagent.v1.PingSandboxRequest 29, // 41: hostagent.v1.HostAgentService.ReadFileStream:input_type -> hostagent.v1.ReadFileStreamRequest
40, // 42: hostagent.v1.HostAgentService.Terminate:input_type -> hostagent.v1.TerminateRequest 38, // 42: hostagent.v1.HostAgentService.PingSandbox:input_type -> hostagent.v1.PingSandboxRequest
43, // 43: hostagent.v1.HostAgentService.GetSandboxMetrics:input_type -> hostagent.v1.GetSandboxMetricsRequest 40, // 43: hostagent.v1.HostAgentService.Terminate:input_type -> hostagent.v1.TerminateRequest
45, // 44: hostagent.v1.HostAgentService.FlushSandboxMetrics:input_type -> hostagent.v1.FlushSandboxMetricsRequest 43, // 44: hostagent.v1.HostAgentService.GetSandboxMetrics:input_type -> hostagent.v1.GetSandboxMetricsRequest
47, // 45: hostagent.v1.HostAgentService.FlattenRootfs:input_type -> hostagent.v1.FlattenRootfsRequest 45, // 45: hostagent.v1.HostAgentService.FlushSandboxMetrics:input_type -> hostagent.v1.FlushSandboxMetricsRequest
49, // 46: hostagent.v1.HostAgentService.PtyAttach:input_type -> hostagent.v1.PtyAttachRequest 47, // 46: hostagent.v1.HostAgentService.FlattenRootfs:input_type -> hostagent.v1.FlattenRootfsRequest
54, // 47: hostagent.v1.HostAgentService.PtySendInput:input_type -> hostagent.v1.PtySendInputRequest 49, // 47: hostagent.v1.HostAgentService.PtyAttach:input_type -> hostagent.v1.PtyAttachRequest
56, // 48: hostagent.v1.HostAgentService.PtyResize:input_type -> hostagent.v1.PtyResizeRequest 54, // 48: hostagent.v1.HostAgentService.PtySendInput:input_type -> hostagent.v1.PtySendInputRequest
58, // 49: hostagent.v1.HostAgentService.PtyKill:input_type -> hostagent.v1.PtyKillRequest 56, // 49: hostagent.v1.HostAgentService.PtyResize:input_type -> hostagent.v1.PtyResizeRequest
60, // 50: hostagent.v1.HostAgentService.StartBackground:input_type -> hostagent.v1.StartBackgroundRequest 58, // 50: hostagent.v1.HostAgentService.PtyKill:input_type -> hostagent.v1.PtyKillRequest
62, // 51: hostagent.v1.HostAgentService.ListProcesses:input_type -> hostagent.v1.ListProcessesRequest 60, // 51: hostagent.v1.HostAgentService.StartBackground:input_type -> hostagent.v1.StartBackgroundRequest
65, // 52: hostagent.v1.HostAgentService.KillProcess:input_type -> hostagent.v1.KillProcessRequest 62, // 52: hostagent.v1.HostAgentService.ListProcesses:input_type -> hostagent.v1.ListProcessesRequest
67, // 53: hostagent.v1.HostAgentService.ConnectProcess:input_type -> hostagent.v1.ConnectProcessRequest 65, // 53: hostagent.v1.HostAgentService.KillProcess:input_type -> hostagent.v1.KillProcessRequest
1, // 54: hostagent.v1.HostAgentService.CreateSandbox:output_type -> hostagent.v1.CreateSandboxResponse 67, // 54: hostagent.v1.HostAgentService.ConnectProcess:input_type -> hostagent.v1.ConnectProcessRequest
3, // 55: hostagent.v1.HostAgentService.DestroySandbox:output_type -> hostagent.v1.DestroySandboxResponse 1, // 55: hostagent.v1.HostAgentService.CreateSandbox:output_type -> hostagent.v1.CreateSandboxResponse
5, // 56: hostagent.v1.HostAgentService.PauseSandbox:output_type -> hostagent.v1.PauseSandboxResponse 3, // 56: hostagent.v1.HostAgentService.DestroySandbox:output_type -> hostagent.v1.DestroySandboxResponse
7, // 57: hostagent.v1.HostAgentService.ResumeSandbox:output_type -> hostagent.v1.ResumeSandboxResponse 5, // 57: hostagent.v1.HostAgentService.PauseSandbox:output_type -> hostagent.v1.PauseSandboxResponse
13, // 58: hostagent.v1.HostAgentService.Exec:output_type -> hostagent.v1.ExecResponse 7, // 58: hostagent.v1.HostAgentService.ResumeSandbox:output_type -> hostagent.v1.ResumeSandboxResponse
15, // 59: hostagent.v1.HostAgentService.ListSandboxes:output_type -> hostagent.v1.ListSandboxesResponse 13, // 59: hostagent.v1.HostAgentService.Exec:output_type -> hostagent.v1.ExecResponse
18, // 60: hostagent.v1.HostAgentService.WriteFile:output_type -> hostagent.v1.WriteFileResponse 15, // 60: hostagent.v1.HostAgentService.ListSandboxes:output_type -> hostagent.v1.ListSandboxesResponse
20, // 61: hostagent.v1.HostAgentService.ReadFile:output_type -> hostagent.v1.ReadFileResponse 18, // 61: hostagent.v1.HostAgentService.WriteFile:output_type -> hostagent.v1.WriteFileResponse
32, // 62: hostagent.v1.HostAgentService.ListDir:output_type -> hostagent.v1.ListDirResponse 20, // 62: hostagent.v1.HostAgentService.ReadFile:output_type -> hostagent.v1.ReadFileResponse
35, // 63: hostagent.v1.HostAgentService.MakeDir:output_type -> hostagent.v1.MakeDirResponse 32, // 63: hostagent.v1.HostAgentService.ListDir:output_type -> hostagent.v1.ListDirResponse
37, // 64: hostagent.v1.HostAgentService.RemovePath:output_type -> hostagent.v1.RemovePathResponse 35, // 64: hostagent.v1.HostAgentService.MakeDir:output_type -> hostagent.v1.MakeDirResponse
9, // 65: hostagent.v1.HostAgentService.CreateSnapshot:output_type -> hostagent.v1.CreateSnapshotResponse 37, // 65: hostagent.v1.HostAgentService.RemovePath:output_type -> hostagent.v1.RemovePathResponse
11, // 66: hostagent.v1.HostAgentService.DeleteSnapshot:output_type -> hostagent.v1.DeleteSnapshotResponse 9, // 66: hostagent.v1.HostAgentService.CreateSnapshot:output_type -> hostagent.v1.CreateSnapshotResponse
22, // 67: hostagent.v1.HostAgentService.ExecStream:output_type -> hostagent.v1.ExecStreamResponse 11, // 67: hostagent.v1.HostAgentService.DeleteSnapshot:output_type -> hostagent.v1.DeleteSnapshotResponse
28, // 68: hostagent.v1.HostAgentService.WriteFileStream:output_type -> hostagent.v1.WriteFileStreamResponse 22, // 68: hostagent.v1.HostAgentService.ExecStream:output_type -> hostagent.v1.ExecStreamResponse
30, // 69: hostagent.v1.HostAgentService.ReadFileStream:output_type -> hostagent.v1.ReadFileStreamResponse 28, // 69: hostagent.v1.HostAgentService.WriteFileStream:output_type -> hostagent.v1.WriteFileStreamResponse
39, // 70: hostagent.v1.HostAgentService.PingSandbox:output_type -> hostagent.v1.PingSandboxResponse 30, // 70: hostagent.v1.HostAgentService.ReadFileStream:output_type -> hostagent.v1.ReadFileStreamResponse
41, // 71: hostagent.v1.HostAgentService.Terminate:output_type -> hostagent.v1.TerminateResponse 39, // 71: hostagent.v1.HostAgentService.PingSandbox:output_type -> hostagent.v1.PingSandboxResponse
44, // 72: hostagent.v1.HostAgentService.GetSandboxMetrics:output_type -> hostagent.v1.GetSandboxMetricsResponse 41, // 72: hostagent.v1.HostAgentService.Terminate:output_type -> hostagent.v1.TerminateResponse
46, // 73: hostagent.v1.HostAgentService.FlushSandboxMetrics:output_type -> hostagent.v1.FlushSandboxMetricsResponse 44, // 73: hostagent.v1.HostAgentService.GetSandboxMetrics:output_type -> hostagent.v1.GetSandboxMetricsResponse
48, // 74: hostagent.v1.HostAgentService.FlattenRootfs:output_type -> hostagent.v1.FlattenRootfsResponse 46, // 74: hostagent.v1.HostAgentService.FlushSandboxMetrics:output_type -> hostagent.v1.FlushSandboxMetricsResponse
50, // 75: hostagent.v1.HostAgentService.PtyAttach:output_type -> hostagent.v1.PtyAttachResponse 48, // 75: hostagent.v1.HostAgentService.FlattenRootfs:output_type -> hostagent.v1.FlattenRootfsResponse
55, // 76: hostagent.v1.HostAgentService.PtySendInput:output_type -> hostagent.v1.PtySendInputResponse 50, // 76: hostagent.v1.HostAgentService.PtyAttach:output_type -> hostagent.v1.PtyAttachResponse
57, // 77: hostagent.v1.HostAgentService.PtyResize:output_type -> hostagent.v1.PtyResizeResponse 55, // 77: hostagent.v1.HostAgentService.PtySendInput:output_type -> hostagent.v1.PtySendInputResponse
59, // 78: hostagent.v1.HostAgentService.PtyKill:output_type -> hostagent.v1.PtyKillResponse 57, // 78: hostagent.v1.HostAgentService.PtyResize:output_type -> hostagent.v1.PtyResizeResponse
61, // 79: hostagent.v1.HostAgentService.StartBackground:output_type -> hostagent.v1.StartBackgroundResponse 59, // 79: hostagent.v1.HostAgentService.PtyKill:output_type -> hostagent.v1.PtyKillResponse
64, // 80: hostagent.v1.HostAgentService.ListProcesses:output_type -> hostagent.v1.ListProcessesResponse 61, // 80: hostagent.v1.HostAgentService.StartBackground:output_type -> hostagent.v1.StartBackgroundResponse
66, // 81: hostagent.v1.HostAgentService.KillProcess:output_type -> hostagent.v1.KillProcessResponse 64, // 81: hostagent.v1.HostAgentService.ListProcesses:output_type -> hostagent.v1.ListProcessesResponse
68, // 82: hostagent.v1.HostAgentService.ConnectProcess:output_type -> hostagent.v1.ConnectProcessResponse 66, // 82: hostagent.v1.HostAgentService.KillProcess:output_type -> hostagent.v1.KillProcessResponse
54, // [54:83] is the sub-list for method output_type 68, // 83: hostagent.v1.HostAgentService.ConnectProcess:output_type -> hostagent.v1.ConnectProcessResponse
25, // [25:54] is the sub-list for method input_type 55, // [55:84] is the sub-list for method output_type
25, // [25:25] is the sub-list for extension type_name 26, // [26:55] is the sub-list for method input_type
25, // [25:25] is the sub-list for extension extendee 26, // [26:26] is the sub-list for extension type_name
0, // [0:25] is the sub-list for field type_name 26, // [26:26] is the sub-list for extension extendee
0, // [0:26] is the sub-list for field type_name
} }
func init() { file_hostagent_proto_init() } func init() { file_hostagent_proto_init() }
@ -4699,7 +4724,7 @@ func file_hostagent_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_hostagent_proto_rawDesc), len(file_hostagent_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_hostagent_proto_rawDesc), len(file_hostagent_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 76, NumMessages: 77,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@ -222,6 +222,10 @@ message ExecRequest {
repeated string args = 3; repeated string args = 3;
// Timeout for the command in seconds (default: 30). // Timeout for the command in seconds (default: 30).
int32 timeout_sec = 4; int32 timeout_sec = 4;
// Environment variables to set for the command.
map<string, string> envs = 5;
// Working directory for the command.
string cwd = 6;
} }
message ExecResponse { message ExecResponse {