mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-22 15:11:02 +00:00
178 lines
4.8 KiB
Go
178 lines
4.8 KiB
Go
package commands
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
cliconfig "github.com/docker/cli/cli/config"
|
|
ddocker "github.com/docker/cli/cli/context/docker"
|
|
ctxstore "github.com/docker/cli/cli/context/store"
|
|
"github.com/docker/docker/client"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var (
|
|
ErrNoDockerSocket = fmt.Errorf("no working Docker/Podman socket found")
|
|
)
|
|
|
|
// Timeout for validating socket connectivity
|
|
const socketValidationTimeout = 3 * time.Second
|
|
|
|
var (
|
|
validateSocketFunc = validateSocket
|
|
)
|
|
|
|
// Runtime type detection
|
|
type ContainerRuntime string
|
|
|
|
const (
|
|
RuntimeDocker ContainerRuntime = "docker"
|
|
RuntimePodman ContainerRuntime = "podman"
|
|
RuntimeUnknown ContainerRuntime = "unknown"
|
|
)
|
|
|
|
// Cache for socket detection results
|
|
var (
|
|
cachedDockerHost string
|
|
cachedRuntime ContainerRuntime
|
|
dockerHostMu sync.Mutex
|
|
)
|
|
|
|
// DetectDockerHost finds a working Docker/Podman socket
|
|
// Results are cached after first successful detection
|
|
func DetectDockerHost(log *logrus.Entry) (string, ContainerRuntime, error) {
|
|
dockerHostMu.Lock()
|
|
defer dockerHostMu.Unlock()
|
|
|
|
if cachedDockerHost != "" {
|
|
return cachedDockerHost, cachedRuntime, nil
|
|
}
|
|
|
|
host, runtime, err := detectDockerHostInternal(log)
|
|
if err != nil {
|
|
return "", RuntimeUnknown, err
|
|
}
|
|
|
|
cachedDockerHost = host
|
|
cachedRuntime = runtime
|
|
return host, runtime, nil
|
|
}
|
|
|
|
// ResetDockerHostCache resets the cached docker host. Used for testing.
|
|
func ResetDockerHostCache() {
|
|
dockerHostMu.Lock()
|
|
defer dockerHostMu.Unlock()
|
|
cachedDockerHost = ""
|
|
cachedRuntime = ""
|
|
}
|
|
|
|
func detectDockerHostInternal(log *logrus.Entry) (string, ContainerRuntime, error) {
|
|
// Priority 1: Explicit DOCKER_HOST environment variable
|
|
if dockerHost := os.Getenv("DOCKER_HOST"); dockerHost != "" {
|
|
log.Debugf("Using DOCKER_HOST from environment: %s", dockerHost)
|
|
|
|
// Handle plain paths without schema
|
|
if !strings.Contains(dockerHost, "://") {
|
|
if _, err := os.Stat(dockerHost); err == nil {
|
|
log.Debugf("DOCKER_HOST is a plain path, assuming unix://")
|
|
dockerHost = "unix://" + dockerHost
|
|
}
|
|
}
|
|
|
|
if !strings.HasPrefix(dockerHost, "ssh://") {
|
|
ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout)
|
|
defer cancel()
|
|
if err := validateSocketFunc(ctx, dockerHost, true); err != nil {
|
|
log.Warnf("DOCKER_HOST=%s is set but not accessible: %v", dockerHost, err)
|
|
}
|
|
}
|
|
return dockerHost, RuntimeUnknown, nil
|
|
}
|
|
|
|
// Priority 2: Docker Context
|
|
contextHost, err := getHostFromContext()
|
|
if err != nil {
|
|
// If DOCKER_CONTEXT was explicitly set, we should fail
|
|
if os.Getenv("DOCKER_CONTEXT") != "" {
|
|
return "", RuntimeUnknown, fmt.Errorf("failed to use DOCKER_CONTEXT: %w", err)
|
|
}
|
|
log.Debugf("Failed to get host from default context: %v", err)
|
|
} else if contextHost != "" {
|
|
log.Debugf("Using host from Docker context: %s", contextHost)
|
|
if !strings.HasPrefix(contextHost, "ssh://") {
|
|
ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout)
|
|
defer cancel()
|
|
if err := validateSocketFunc(ctx, contextHost, false); err != nil {
|
|
log.Warnf("Context host %s is not accessible: %v", contextHost, err)
|
|
}
|
|
}
|
|
return contextHost, RuntimeUnknown, nil
|
|
}
|
|
|
|
// Priority 3: Platform-specific candidates
|
|
return detectPlatformCandidates(log)
|
|
}
|
|
|
|
// getHostFromContext retrieves the host from the current Docker context
|
|
func getHostFromContext() (string, error) {
|
|
currentContext := os.Getenv("DOCKER_CONTEXT")
|
|
if currentContext == "" {
|
|
cf, err := cliconfig.Load(cliconfig.Dir())
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
currentContext = cf.CurrentContext
|
|
}
|
|
|
|
if currentContext == "" || currentContext == "default" {
|
|
return "", nil
|
|
}
|
|
|
|
storeConfig := ctxstore.NewConfig(
|
|
func() interface{} { return &ddocker.EndpointMeta{} },
|
|
ctxstore.EndpointTypeGetter(ddocker.DockerEndpoint, func() interface{} { return &ddocker.EndpointMeta{} }),
|
|
)
|
|
|
|
st := ctxstore.New(cliconfig.ContextStoreDir(), storeConfig)
|
|
md, err := st.GetMetadata(currentContext)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
dockerEP, ok := md.Endpoints[ddocker.DockerEndpoint]
|
|
if !ok {
|
|
return "", nil
|
|
}
|
|
dockerEPMeta, ok := dockerEP.(ddocker.EndpointMeta)
|
|
if !ok {
|
|
return "", fmt.Errorf("expected docker.EndpointMeta, got %T", dockerEP)
|
|
}
|
|
|
|
return dockerEPMeta.Host, nil
|
|
}
|
|
|
|
// validateSocket attempts to connect to the Docker API at the given host
|
|
func validateSocket(ctx context.Context, host string, useEnv bool) error {
|
|
var opts []client.Opt
|
|
if useEnv {
|
|
// If we're validating the host from the environment, use FromEnv to pick up TLS settings
|
|
opts = append(opts, client.FromEnv)
|
|
}
|
|
opts = append(opts, client.WithHost(host), client.WithAPIVersionNegotiation())
|
|
|
|
cli, err := client.NewClientWithOpts(opts...)
|
|
if err != nil {
|
|
return fmt.Errorf("create client: %w", err)
|
|
}
|
|
defer cli.Close()
|
|
|
|
_, err = cli.Ping(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("ping failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|