fix: enhance DockerUnavailableError to include detailed error messages and improve handling in ListContainers
This commit is contained in:
@@ -7,6 +7,8 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
@@ -16,11 +18,17 @@ import (
|
||||
)
|
||||
|
||||
type DockerUnavailableError struct {
|
||||
err error
|
||||
err error
|
||||
details string
|
||||
}
|
||||
|
||||
func NewDockerUnavailableError(err error) *DockerUnavailableError {
|
||||
return &DockerUnavailableError{err: err}
|
||||
func NewDockerUnavailableError(err error, details ...string) *DockerUnavailableError {
|
||||
detailMsg := ""
|
||||
if len(details) > 0 {
|
||||
detailMsg = details[0]
|
||||
}
|
||||
|
||||
return &DockerUnavailableError{err: err, details: detailMsg}
|
||||
}
|
||||
|
||||
func (e *DockerUnavailableError) Error() string {
|
||||
@@ -37,6 +45,13 @@ func (e *DockerUnavailableError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func (e *DockerUnavailableError) Details() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return e.details
|
||||
}
|
||||
|
||||
type DockerPort struct {
|
||||
PrivatePort uint16 `json:"private_port"`
|
||||
PublicPort uint16 `json:"public_port"`
|
||||
@@ -55,8 +70,9 @@ type DockerContainer struct {
|
||||
}
|
||||
|
||||
type DockerService struct {
|
||||
client *client.Client
|
||||
initErr error // Stores initialization error if Docker is unavailable
|
||||
client *client.Client
|
||||
initErr error // Stores initialization error if Docker is unavailable
|
||||
localHost string
|
||||
}
|
||||
|
||||
// NewDockerService creates a new Docker service instance.
|
||||
@@ -64,21 +80,33 @@ type DockerService struct {
|
||||
// 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())
|
||||
envHost := strings.TrimSpace(os.Getenv("DOCKER_HOST"))
|
||||
localHost := resolveLocalDockerHost()
|
||||
if envHost != "" && !strings.HasPrefix(envHost, "unix://") {
|
||||
logger.Log().WithFields(map[string]any{"docker_host_env": envHost, "local_host": localHost}).Info("ignoring non-unix DOCKER_HOST for local docker mode")
|
||||
}
|
||||
|
||||
cli, err := client.NewClientWithOpts(client.WithHost(localHost), client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
logger.Log().WithError(err).Warn("Failed to initialize Docker client - Docker features will be unavailable")
|
||||
unavailableErr := NewDockerUnavailableError(err, buildLocalDockerUnavailableDetails(err, localHost))
|
||||
return &DockerService{
|
||||
client: nil,
|
||||
initErr: err,
|
||||
client: nil,
|
||||
initErr: unavailableErr,
|
||||
localHost: localHost,
|
||||
}
|
||||
}
|
||||
return &DockerService{client: cli, initErr: nil}
|
||||
return &DockerService{client: cli, initErr: nil, localHost: localHost}
|
||||
}
|
||||
|
||||
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 unavailableErr *DockerUnavailableError
|
||||
if errors.As(s.initErr, &unavailableErr) {
|
||||
return nil, unavailableErr
|
||||
}
|
||||
return nil, NewDockerUnavailableError(s.initErr, buildLocalDockerUnavailableDetails(s.initErr, s.localHost))
|
||||
}
|
||||
|
||||
var cli *client.Client
|
||||
@@ -101,7 +129,10 @@ func (s *DockerService) ListContainers(ctx context.Context, host string) ([]Dock
|
||||
containers, err := cli.ContainerList(ctx, container.ListOptions{All: false})
|
||||
if err != nil {
|
||||
if isDockerConnectivityError(err) {
|
||||
return nil, &DockerUnavailableError{err: err}
|
||||
if host == "" || host == "local" {
|
||||
return nil, NewDockerUnavailableError(err, buildLocalDockerUnavailableDetails(err, s.localHost))
|
||||
}
|
||||
return nil, NewDockerUnavailableError(err)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to list containers: %w", err)
|
||||
}
|
||||
@@ -206,3 +237,118 @@ func isDockerConnectivityError(err error) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func resolveLocalDockerHost() string {
|
||||
envHost := strings.TrimSpace(os.Getenv("DOCKER_HOST"))
|
||||
if strings.HasPrefix(envHost, "unix://") {
|
||||
socketPath := socketPathFromDockerHost(envHost)
|
||||
if socketPath != "" {
|
||||
if _, err := os.Stat(socketPath); err == nil {
|
||||
return envHost
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultSocketPath := "/var/run/docker.sock"
|
||||
if _, err := os.Stat(defaultSocketPath); err == nil {
|
||||
return "unix:///var/run/docker.sock"
|
||||
}
|
||||
|
||||
rootlessSocketPath := fmt.Sprintf("/run/user/%d/docker.sock", os.Getuid())
|
||||
if _, err := os.Stat(rootlessSocketPath); err == nil {
|
||||
return "unix://" + rootlessSocketPath
|
||||
}
|
||||
|
||||
return "unix:///var/run/docker.sock"
|
||||
}
|
||||
|
||||
func socketPathFromDockerHost(host string) string {
|
||||
trimmedHost := strings.TrimSpace(host)
|
||||
if !strings.HasPrefix(trimmedHost, "unix://") {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimPrefix(trimmedHost, "unix://")
|
||||
}
|
||||
|
||||
func buildLocalDockerUnavailableDetails(err error, localHost string) string {
|
||||
socketPath := socketPathFromDockerHost(localHost)
|
||||
if socketPath == "" {
|
||||
socketPath = "/var/run/docker.sock"
|
||||
}
|
||||
|
||||
uid := os.Getuid()
|
||||
gid := os.Getgid()
|
||||
groups, _ := os.Getgroups()
|
||||
groupsStr := ""
|
||||
if len(groups) > 0 {
|
||||
groupValues := make([]string, 0, len(groups))
|
||||
for _, groupID := range groups {
|
||||
groupValues = append(groupValues, strconv.Itoa(groupID))
|
||||
}
|
||||
groupsStr = strings.Join(groupValues, ",")
|
||||
}
|
||||
|
||||
if errno, ok := extractErrno(err); ok {
|
||||
switch errno {
|
||||
case syscall.ENOENT:
|
||||
return fmt.Sprintf("Local Docker socket not found at %s (local host selector uses %s). Mount %s as read-only or read-write.", socketPath, localHost, socketPath)
|
||||
case syscall.ECONNREFUSED:
|
||||
return fmt.Sprintf("Docker daemon is not accepting connections at %s.", socketPath)
|
||||
case syscall.EACCES, syscall.EPERM:
|
||||
infoMsg, socketGID := localSocketStatSummary(socketPath)
|
||||
permissionHint := ""
|
||||
if socketGID >= 0 && !slices.Contains(groups, socketGID) {
|
||||
permissionHint = fmt.Sprintf(" Process groups (%s) do not include socket gid %d; run container with matching supplemental group (e.g., --group-add %d).", groupsStr, socketGID, socketGID)
|
||||
}
|
||||
return fmt.Sprintf("Local Docker socket is mounted but not accessible by current process (uid=%d gid=%d). %s%s", uid, gid, infoMsg, permissionHint)
|
||||
}
|
||||
}
|
||||
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Sprintf("Local Docker socket not found at %s (local host selector uses %s).", socketPath, localHost)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Cannot connect to local Docker via %s. Ensure Docker is running and the mounted socket permissions allow uid=%d gid=%d access.", localHost, uid, gid)
|
||||
}
|
||||
|
||||
func extractErrno(err error) (syscall.Errno, bool) {
|
||||
if err == nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
var urlErr *url.Error
|
||||
if errors.As(err, &urlErr) {
|
||||
err = urlErr.Unwrap()
|
||||
}
|
||||
|
||||
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) {
|
||||
return errno, true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func localSocketStatSummary(socketPath string) (string, int) {
|
||||
info, statErr := os.Stat(socketPath)
|
||||
if statErr != nil {
|
||||
return fmt.Sprintf("Socket path %s could not be stat'ed: %v.", socketPath, statErr), -1
|
||||
}
|
||||
|
||||
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||
if !ok || stat == nil {
|
||||
return fmt.Sprintf("Socket path %s has mode %s.", socketPath, info.Mode().String()), -1
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Socket path %s has mode %s owner uid=%d gid=%d.", socketPath, info.Mode().String(), stat.Uid, stat.Gid), int(stat.Gid)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user