package managed import ( "context" "errors" "fmt" "os/exec" "strings" "time" ) type DockerRuntime interface { GetContainerStatus(containerName string) (string, error) RestartContainer(containerName string) error } type CommandRunner func(ctx context.Context, name string, args ...string) ([]byte, error) type DockerController struct { runner CommandRunner timeout time.Duration } func NewDockerController() *DockerController { return &DockerController{ runner: func(ctx context.Context, name string, args ...string) ([]byte, error) { return exec.CommandContext(ctx, name, args...).CombinedOutput() }, timeout: 5 * time.Second, } } func NewDockerControllerWithRunner(runner CommandRunner) *DockerController { return &DockerController{ runner: runner, timeout: 5 * time.Second, } } func NormalizeContainerStatus(raw string) string { switch strings.TrimSpace(strings.ToLower(raw)) { case "running": return "running" case "created", "exited", "paused": return "stopped" case "dead", "removing", "restarting": return "failed" default: return "unknown" } } func (c *DockerController) GetContainerStatus(containerName string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), c.timeout) defer cancel() output, err := c.runner(ctx, "docker", "inspect", "--format", "{{.State.Status}}", containerName) status := NormalizeContainerStatus(string(output)) if status != "unknown" { return status, nil } if err != nil { return status, fmt.Errorf("docker inspect %s: %w", containerName, err) } return status, nil } func (c *DockerController) RestartContainer(containerName string) error { ctx, cancel := context.WithTimeout(context.Background(), c.timeout) defer cancel() output, err := c.runner(ctx, "docker", "restart", containerName) if err != nil { trimmed := strings.TrimSpace(string(output)) if trimmed != "" { return fmt.Errorf("docker restart %s: %w: %s", containerName, err, trimmed) } return fmt.Errorf("docker restart %s: %w", containerName, err) } return nil } func IsDockerUnavailable(err error) bool { if err == nil { return false } var execErr *exec.Error if errors.As(err, &execErr) { return execErr.Err == exec.ErrNotFound } message := err.Error() return errors.Is(err, exec.ErrNotFound) || strings.Contains(message, `exec: "docker": executable file not found`) || strings.Contains(message, "failed to connect to the docker API") || strings.Contains(message, "docker.sock") }