Files
kandev/apps/backend/internal/agent/runtime/lifecycle/manager_environment.go
T
2026-05-16 20:07:57 +01:00

125 lines
4.5 KiB
Go

package lifecycle
import (
"context"
"fmt"
"time"
"github.com/kandev/kandev/internal/agent/executor"
)
// ContainerLiveStatus is a runtime snapshot of a Docker container's state,
// suitable for surfacing in the Executor Settings popover.
type ContainerLiveStatus struct {
ContainerID string `json:"container_id"`
State string `json:"state"` // running, exited, paused, restarting, removing, dead, created
Status string `json:"status"` // human-readable, e.g. "Up 5 minutes"
StartedAt *time.Time `json:"started_at,omitempty"`
FinishedAt *time.Time `json:"finished_at,omitempty"`
ExitCode int `json:"exit_code,omitempty"`
Health string `json:"health,omitempty"`
Missing bool `json:"missing,omitempty"` // true when the container no longer exists
}
// DestroyContainer removes a Docker container (forcefully, along with its filesystem).
// Used by Reset Environment to tear down the container layer without touching worktrees
// or sprite sandboxes.
func (m *Manager) DestroyContainer(ctx context.Context, containerID string) error {
if containerID == "" {
return nil
}
backend, err := m.executorRegistry.GetBackend(executor.NameDocker)
if err != nil {
return fmt.Errorf("docker backend unavailable: %w", err)
}
dockerExec, ok := backend.(*DockerExecutor)
if !ok {
return fmt.Errorf("docker backend has unexpected type %T", backend)
}
// Force lazy initialization here. After a backend restart this method
// can be the first caller to touch Docker, and a missing daemon should
// surface as a clear "docker unavailable" error rather than a generic
// "container manager not initialized".
if _, _, err := dockerExec.ensureClient(); err != nil {
return fmt.Errorf("initialize docker backend: %w", err)
}
cm := dockerExec.ContainerMgr()
if cm == nil {
return fmt.Errorf("docker container manager not initialized")
}
return cm.RemoveContainer(ctx, containerID, true)
}
// GetContainerLiveStatus inspects a Docker container and returns a live
// snapshot. Returns Missing=true when the container has been removed.
func (m *Manager) GetContainerLiveStatus(ctx context.Context, containerID string) (*ContainerLiveStatus, error) {
if containerID == "" {
return nil, nil
}
backend, err := m.executorRegistry.GetBackend(executor.NameDocker)
if err != nil {
return nil, fmt.Errorf("docker backend unavailable: %w", err)
}
dockerExec, ok := backend.(*DockerExecutor)
if !ok {
return nil, fmt.Errorf("docker backend has unexpected type %T", backend)
}
if _, _, err := dockerExec.ensureClient(); err != nil {
return nil, fmt.Errorf("initialize docker backend: %w", err)
}
cm := dockerExec.ContainerMgr()
if cm == nil {
return nil, fmt.Errorf("docker container manager not initialized")
}
info, err := cm.GetContainerInfo(ctx, containerID)
if err != nil {
// Treat any inspect failure as "missing" so the UI can surface it
// without bubbling a 500 to the popover.
return &ContainerLiveStatus{ContainerID: containerID, State: "missing", Missing: true}, nil
}
out := &ContainerLiveStatus{
ContainerID: containerID,
State: info.State,
Status: info.Status,
ExitCode: info.ExitCode,
Health: info.Health,
}
if !info.StartedAt.IsZero() {
t := info.StartedAt
out.StartedAt = &t
}
if !info.FinishedAt.IsZero() {
t := info.FinishedAt
out.FinishedAt = &t
}
return out, nil
}
// DestroySandbox destroys a Sprites sandbox by name. The executionID is used to
// resolve a cached API token when one exists; if no token is cached, the sprite's
// metadata-resolver fallback kicks in.
func (m *Manager) DestroySandbox(ctx context.Context, sandboxID, executionID string) error {
if sandboxID == "" {
return nil
}
backend, err := m.executorRegistry.GetBackend(executor.NameSprites)
if err != nil {
return fmt.Errorf("sprites backend unavailable: %w", err)
}
spritesExec, ok := backend.(*SpritesExecutor)
if !ok {
return fmt.Errorf("sprites backend has unexpected type %T", backend)
}
// StopInstance only runs sandbox cleanup when StopReason matches a
// terminal reason (see shouldRunExecutorCleanup). DestroySandbox IS the
// destruction path, so set StopReasonTaskDeleted explicitly — the
// empty StopReason this previously sent was silently treated as a
// preserve-on-stop signal and the sandbox was never destroyed.
instance := &ExecutorInstance{
InstanceID: executionID,
Metadata: map[string]interface{}{MetadataKeySpriteName: sandboxID},
StopReason: StopReasonTaskDeleted,
}
return spritesExec.StopInstance(ctx, instance, true)
}