package services import ( "context" "errors" "fmt" "net" "net/url" "os" "strings" "syscall" "github.com/Wikid82/charon/backend/internal/logger" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" ) type DockerUnavailableError struct { err error } func NewDockerUnavailableError(err error) *DockerUnavailableError { return &DockerUnavailableError{err: err} } func (e *DockerUnavailableError) Error() string { if e == nil || e.err == nil { return "docker unavailable" } return fmt.Sprintf("docker unavailable: %v", e.err) } func (e *DockerUnavailableError) Unwrap() error { if e == nil { return nil } return e.err } type DockerPort struct { PrivatePort uint16 `json:"private_port"` PublicPort uint16 `json:"public_port"` Type string `json:"type"` } type DockerContainer struct { ID string `json:"id"` Names []string `json:"names"` Image string `json:"image"` State string `json:"state"` Status string `json:"status"` Network string `json:"network"` IP string `json:"ip"` Ports []DockerPort `json:"ports"` } type DockerService struct { client *client.Client initErr error // Stores initialization error if Docker is unavailable } // NewDockerService creates a new Docker service instance. // If Docker client initialization fails, it returns a stub service that will return // DockerUnavailableError for all operations. This allows routes to be registered // and provide helpful error messages to users. func NewDockerService() *DockerService { cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { logger.Log().WithError(err).Warn("Failed to initialize Docker client - Docker features will be unavailable") return &DockerService{ client: nil, initErr: err, } } return &DockerService{client: cli, initErr: nil} } func (s *DockerService) ListContainers(ctx context.Context, host string) ([]DockerContainer, error) { // Check if Docker was available during initialization if s.initErr != nil { return nil, &DockerUnavailableError{err: s.initErr} } var cli *client.Client var err error if host == "" || host == "local" { cli = s.client } else { cli, err = client.NewClientWithOpts(client.WithHost(host), client.WithAPIVersionNegotiation()) if err != nil { return nil, fmt.Errorf("failed to create remote client: %w", err) } defer func() { if err := cli.Close(); err != nil { logger.Log().WithError(err).Warn("failed to close docker client") } }() } containers, err := cli.ContainerList(ctx, container.ListOptions{All: false}) if err != nil { if isDockerConnectivityError(err) { return nil, &DockerUnavailableError{err: err} } return nil, fmt.Errorf("failed to list containers: %w", err) } var result []DockerContainer for _, c := range containers { // Get the first network's IP address if available networkName := "" ipAddress := "" if c.NetworkSettings != nil && len(c.NetworkSettings.Networks) > 0 { for name, net := range c.NetworkSettings.Networks { networkName = name ipAddress = net.IPAddress break // Just take the first one for now } } // Clean up names (remove leading slash) names := make([]string, len(c.Names)) for i, name := range c.Names { names[i] = strings.TrimPrefix(name, "/") } // Map ports var ports []DockerPort for _, p := range c.Ports { ports = append(ports, DockerPort{ PrivatePort: p.PrivatePort, PublicPort: p.PublicPort, Type: p.Type, }) } result = append(result, DockerContainer{ ID: c.ID[:12], // Short ID Names: names, Image: c.Image, State: c.State, Status: c.Status, Network: networkName, IP: ipAddress, Ports: ports, }) } return result, nil } func isDockerConnectivityError(err error) bool { if err == nil { return false } // Common high-signal strings from docker client/daemon failures. msg := strings.ToLower(err.Error()) if strings.Contains(msg, "cannot connect to the docker daemon") || strings.Contains(msg, "is the docker daemon running") || strings.Contains(msg, "error during connect") { return true } // Context timeouts typically indicate the daemon/socket is unreachable. if errors.Is(err, context.DeadlineExceeded) { return true } var urlErr *url.Error if errors.As(err, &urlErr) { err = urlErr.Unwrap() } var netErr net.Error if errors.As(err, &netErr) { if netErr.Timeout() { return true } } // Walk common syscall error wrappers. var syscallErr *os.SyscallError if errors.As(err, &syscallErr) { err = syscallErr.Unwrap() } var opErr *net.OpError if errors.As(err, &opErr) { err = opErr.Unwrap() } var errno syscall.Errno if errors.As(err, &errno) { switch errno { case syscall.ENOENT, syscall.EACCES, syscall.EPERM, syscall.ECONNREFUSED: return true } } // os.ErrNotExist covers missing unix socket paths. if errors.Is(err, os.ErrNotExist) { return true } return false }