mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-25 08:31:03 +00:00
Implement proper detection with Podman support and enhance error handling
Signed-off-by: DeiAsPie <93835541+DeiAsPie@users.noreply.github.com>
This commit is contained in:
parent
f4fc3669ca
commit
12a6289bef
7 changed files with 443 additions and 78 deletions
|
|
@ -13,9 +13,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/imdario/mergo"
|
"github.com/imdario/mergo"
|
||||||
|
|
@ -72,11 +69,15 @@ func (c *DockerCommand) NewCommandObject(obj CommandObject) CommandObject {
|
||||||
|
|
||||||
// NewDockerCommand it runs docker commands
|
// NewDockerCommand it runs docker commands
|
||||||
func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*DockerCommand, error) {
|
func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*DockerCommand, error) {
|
||||||
dockerHost, err := determineDockerHost()
|
// Use new detection with caching and validation
|
||||||
|
dockerHost, runtime, err := DetectDockerHost(log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ogLog.Printf("> could not determine host %v", err)
|
// Provide user-friendly error
|
||||||
|
return nil, fmt.Errorf("cannot connect to container runtime: %w; Troubleshooting: Docker - ensure Docker daemon is running; Podman - run 'systemctl --user enable --now podman.socket'; or set DOCKER_HOST environment variable explicitly", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Infof("Detected %s runtime at %s", runtime, dockerHost)
|
||||||
|
|
||||||
// NOTE: Inject the determined docker host to the environment. This allows the
|
// NOTE: Inject the determined docker host to the environment. This allows the
|
||||||
// `SSHHandler.HandleSSHDockerHost()` to create a local unix socket tunneled
|
// `SSHHandler.HandleSSHDockerHost()` to create a local unix socket tunneled
|
||||||
// over SSH to the specified ssh host.
|
// over SSH to the specified ssh host.
|
||||||
|
|
@ -362,64 +363,3 @@ func (c *DockerCommand) DockerComposeConfig() string {
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
// determineDockerHost tries to the determine the docker host that we should connect to
|
|
||||||
// in the following order of decreasing precedence:
|
|
||||||
// - value of "DOCKER_HOST" environment variable
|
|
||||||
// - host retrieved from the current context (specified via DOCKER_CONTEXT)
|
|
||||||
// - "default docker host" for the host operating system, otherwise
|
|
||||||
func determineDockerHost() (string, error) {
|
|
||||||
// If the docker host is explicitly set via the "DOCKER_HOST" environment variable,
|
|
||||||
// then its a no-brainer :shrug:
|
|
||||||
if os.Getenv("DOCKER_HOST") != "" {
|
|
||||||
return os.Getenv("DOCKER_HOST"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
currentContext := os.Getenv("DOCKER_CONTEXT")
|
|
||||||
if currentContext == "" {
|
|
||||||
cf, err := cliconfig.Load(cliconfig.Dir())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
currentContext = cf.CurrentContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// On some systems (windows) `default` is stored in the docker config as the currentContext.
|
|
||||||
if currentContext == "" || currentContext == "default" {
|
|
||||||
// If a docker context is neither specified via the "DOCKER_CONTEXT" environment variable nor via the
|
|
||||||
// $HOME/.docker/config file, then we fall back to connecting to the "default docker host" meant for
|
|
||||||
// the host operating system.
|
|
||||||
return defaultDockerHost, 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 "", err
|
|
||||||
}
|
|
||||||
dockerEPMeta, ok := dockerEP.(ddocker.EndpointMeta)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("expected docker.EndpointMeta, got %T", dockerEP)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dockerEPMeta.Host != "" {
|
|
||||||
return dockerEPMeta.Host, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// We might end up here, if the context was created with the `host` set to an empty value (i.e. '').
|
|
||||||
// For example:
|
|
||||||
// ```sh
|
|
||||||
// docker context create foo --docker "host="
|
|
||||||
// ```
|
|
||||||
// In such scenario, we mimic the `docker` cli and try to connect to the "default docker host".
|
|
||||||
return defaultDockerHost, nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
//go:build !windows
|
|
||||||
|
|
||||||
package commands
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultDockerHost = "unix:///var/run/docker.sock"
|
|
||||||
)
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package commands
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultDockerHost = "npipe:////./pipe/docker_engine"
|
|
||||||
)
|
|
||||||
143
pkg/commands/socket_detection_common.go
Normal file
143
pkg/commands/socket_detection_common.go
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Timeout for validating socket connectivity
|
||||||
|
const socketValidationTimeout = 3 * time.Second
|
||||||
|
|
||||||
|
// 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
|
||||||
|
dockerHostOnce sync.Once
|
||||||
|
dockerHostErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
// DetectDockerHost finds a working Docker/Podman socket
|
||||||
|
// Results are cached after first successful detection
|
||||||
|
func DetectDockerHost(log *logrus.Entry) (string, ContainerRuntime, error) {
|
||||||
|
dockerHostOnce.Do(func() {
|
||||||
|
cachedDockerHost, cachedRuntime, dockerHostErr = detectDockerHostInternal(log)
|
||||||
|
})
|
||||||
|
return cachedDockerHost, cachedRuntime, dockerHostErr
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if !strings.HasPrefix(dockerHost, "ssh://") {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout)
|
||||||
|
defer cancel()
|
||||||
|
if err := validateSocket(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 := validateSocket(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
|
||||||
|
}
|
||||||
102
pkg/commands/socket_detection_test.go
Normal file
102
pkg/commands/socket_detection_test.go
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetSocketCandidates(t *testing.T) {
|
||||||
|
// Save env vars
|
||||||
|
oldXdg := os.Getenv("XDG_RUNTIME_DIR")
|
||||||
|
oldHome := os.Getenv("HOME")
|
||||||
|
defer func() {
|
||||||
|
os.Setenv("XDG_RUNTIME_DIR", oldXdg)
|
||||||
|
os.Setenv("HOME", oldHome)
|
||||||
|
}()
|
||||||
|
|
||||||
|
os.Setenv("XDG_RUNTIME_DIR", "/tmp/runtime")
|
||||||
|
os.Setenv("HOME", "/home/user")
|
||||||
|
|
||||||
|
candidates := getSocketCandidates()
|
||||||
|
|
||||||
|
// Check some expected candidates
|
||||||
|
foundDocker := false
|
||||||
|
foundPodman := false
|
||||||
|
for _, c := range candidates {
|
||||||
|
if c.Path == "unix:///var/run/docker.sock" {
|
||||||
|
foundDocker = true
|
||||||
|
}
|
||||||
|
if c.Path == "unix:///tmp/runtime/podman/podman.sock" {
|
||||||
|
foundPodman = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, foundDocker, "Standard Docker socket should be in candidates")
|
||||||
|
assert.True(t, foundPodman, "Rootless Podman socket should be in candidates")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectDockerHost_DOCKER_HOST_Priority(t *testing.T) {
|
||||||
|
// Save env var
|
||||||
|
oldDockerHost := os.Getenv("DOCKER_HOST")
|
||||||
|
defer os.Setenv("DOCKER_HOST", oldDockerHost)
|
||||||
|
|
||||||
|
expectedHost := "unix:///tmp/custom.sock"
|
||||||
|
os.Setenv("DOCKER_HOST", expectedHost)
|
||||||
|
|
||||||
|
// Reset cache for test
|
||||||
|
dockerHostOnce = sync.Once{}
|
||||||
|
cachedDockerHost = ""
|
||||||
|
|
||||||
|
log := logrus.NewEntry(logrus.New())
|
||||||
|
host, _, err := DetectDockerHost(log)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedHost, host)
|
||||||
|
}
|
||||||
|
func TestDetectDockerHost_Caching(t *testing.T) {
|
||||||
|
// Save env var
|
||||||
|
oldDockerHost := os.Getenv("DOCKER_HOST")
|
||||||
|
defer os.Setenv("DOCKER_HOST", oldDockerHost)
|
||||||
|
|
||||||
|
os.Setenv("DOCKER_HOST", "unix:///tmp/first.sock")
|
||||||
|
|
||||||
|
// Reset cache for test
|
||||||
|
dockerHostOnce = sync.Once{}
|
||||||
|
cachedDockerHost = ""
|
||||||
|
|
||||||
|
log := logrus.NewEntry(logrus.New())
|
||||||
|
host1, _, _ := DetectDockerHost(log)
|
||||||
|
|
||||||
|
// Change env var - should still return first one from cache
|
||||||
|
os.Setenv("DOCKER_HOST", "unix:///tmp/second.sock")
|
||||||
|
host2, _, _ := DetectDockerHost(log)
|
||||||
|
|
||||||
|
assert.Equal(t, host1, host2)
|
||||||
|
assert.Equal(t, "unix:///tmp/first.sock", host2)
|
||||||
|
}
|
||||||
|
func TestDetectDockerHost_Context_Invalid(t *testing.T) {
|
||||||
|
// Save env vars
|
||||||
|
oldDockerHost := os.Getenv("DOCKER_HOST")
|
||||||
|
oldDockerContext := os.Getenv("DOCKER_CONTEXT")
|
||||||
|
defer func() {
|
||||||
|
os.Setenv("DOCKER_HOST", oldDockerHost)
|
||||||
|
os.Setenv("DOCKER_CONTEXT", oldDockerContext)
|
||||||
|
}()
|
||||||
|
|
||||||
|
os.Setenv("DOCKER_HOST", "")
|
||||||
|
os.Setenv("DOCKER_CONTEXT", "nonexistent-context-12345")
|
||||||
|
|
||||||
|
// Reset cache for test
|
||||||
|
dockerHostOnce = sync.Once{}
|
||||||
|
cachedDockerHost = ""
|
||||||
|
|
||||||
|
log := logrus.NewEntry(logrus.New())
|
||||||
|
_, _, err := DetectDockerHost(log)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "failed to use DOCKER_CONTEXT")
|
||||||
|
}
|
||||||
151
pkg/commands/socket_detection_unix.go
Normal file
151
pkg/commands/socket_detection_unix.go
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DockerSocketSchema = "unix://"
|
||||||
|
DockerSocketPath = "/var/run/docker.sock"
|
||||||
|
|
||||||
|
defaultDockerHost = DockerSocketSchema + DockerSocketPath
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoDockerSocket = errors.New("no working Docker/Podman socket found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// SocketCandidate represents a potential socket to try
|
||||||
|
type SocketCandidate struct {
|
||||||
|
Path string
|
||||||
|
Runtime ContainerRuntime
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSocketCandidates returns all possible socket paths in priority order
|
||||||
|
func getSocketCandidates() []SocketCandidate {
|
||||||
|
var candidates []SocketCandidate
|
||||||
|
|
||||||
|
// Helper to add candidate if path is valid
|
||||||
|
addCandidate := func(path string, runtime ContainerRuntime) {
|
||||||
|
if path != "" {
|
||||||
|
candidates = append(candidates, SocketCandidate{
|
||||||
|
Path: DockerSocketSchema + path,
|
||||||
|
Runtime: runtime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Standard Docker daemon socket
|
||||||
|
addCandidate(DockerSocketPath, RuntimeDocker)
|
||||||
|
|
||||||
|
xdgRuntime := os.Getenv("XDG_RUNTIME_DIR")
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
uid := os.Getuid()
|
||||||
|
|
||||||
|
// 2. Rootless Docker: $XDG_RUNTIME_DIR/docker.sock
|
||||||
|
if xdgRuntime != "" {
|
||||||
|
addCandidate(filepath.Join(xdgRuntime, "docker.sock"), RuntimeDocker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Rootless Docker: ~/.docker/run/docker.sock
|
||||||
|
if home != "" {
|
||||||
|
addCandidate(filepath.Join(home, ".docker", "run", "docker.sock"), RuntimeDocker)
|
||||||
|
// 4. Docker Desktop: ~/.docker/desktop/docker.sock
|
||||||
|
addCandidate(filepath.Join(home, ".docker", "desktop", "docker.sock"), RuntimeDocker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Rootless Docker: /run/user/$UID/docker.sock
|
||||||
|
addCandidate(filepath.Join("/run", "user", strconv.Itoa(uid), "docker.sock"), RuntimeDocker)
|
||||||
|
|
||||||
|
// 6. Colima
|
||||||
|
if home != "" {
|
||||||
|
addCandidate(filepath.Join(home, ".colima", "default", "docker.sock"), RuntimeDocker)
|
||||||
|
addCandidate(filepath.Join(home, ".colima", "docker.sock"), RuntimeDocker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. OrbStack
|
||||||
|
if home != "" {
|
||||||
|
addCandidate(filepath.Join(home, ".orbstack", "run", "docker.sock"), RuntimeDocker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Lima
|
||||||
|
if home != "" {
|
||||||
|
addCandidate(filepath.Join(home, ".lima", "default", "sock", "docker.sock"), RuntimeDocker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Rancher Desktop
|
||||||
|
if home != "" {
|
||||||
|
addCandidate(filepath.Join(home, ".rd", "docker.sock"), RuntimeDocker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Snap Docker
|
||||||
|
addCandidate("/var/snap/docker/current/run/docker.sock", RuntimeDocker)
|
||||||
|
|
||||||
|
// 11. Rootless Podman: $XDG_RUNTIME_DIR/podman/podman.sock
|
||||||
|
if xdgRuntime != "" {
|
||||||
|
addCandidate(filepath.Join(xdgRuntime, "podman", "podman.sock"), RuntimePodman)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. Rootless Podman: /run/user/$UID/podman/podman.sock
|
||||||
|
addCandidate(filepath.Join("/run", "user", strconv.Itoa(uid), "podman", "podman.sock"), RuntimePodman)
|
||||||
|
|
||||||
|
// 13. Rootless Podman: ~/.local/share/containers/podman/podman.sock
|
||||||
|
if home != "" {
|
||||||
|
addCandidate(filepath.Join(home, ".local", "share", "containers", "podman", "podman.sock"), RuntimePodman)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 14. Rootful Podman: /run/podman/podman.sock
|
||||||
|
addCandidate("/run/podman/podman.sock", RuntimePodman)
|
||||||
|
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectPlatformCandidates(log *logrus.Entry) (string, ContainerRuntime, error) {
|
||||||
|
var lastErr error
|
||||||
|
candidates := getSocketCandidates()
|
||||||
|
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
socketPath := strings.TrimPrefix(candidate.Path, DockerSocketSchema)
|
||||||
|
|
||||||
|
// Fast path: check if socket file exists
|
||||||
|
if _, err := os.Stat(socketPath); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate by actually connecting
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout)
|
||||||
|
err := validateSocket(ctx, candidate.Path, false)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Socket %s exists but validation failed: %v", candidate.Path, err)
|
||||||
|
if strings.Contains(err.Error(), "permission denied") {
|
||||||
|
lastErr = fmt.Errorf("%s: permission denied (are you in the docker group?)", candidate.Path)
|
||||||
|
} else {
|
||||||
|
lastErr = fmt.Errorf("%s: %w", candidate.Path, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Connected to %s runtime via %s", candidate.Runtime, candidate.Path)
|
||||||
|
return candidate.Path, candidate.Runtime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// All candidates failed - provide actionable error
|
||||||
|
if lastErr != nil {
|
||||||
|
return "", RuntimeUnknown, fmt.Errorf("%w: last error: %v", ErrNoDockerSocket, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := fmt.Sprintf("%v: ensure Docker or Podman is running", ErrNoDockerSocket)
|
||||||
|
return "", RuntimeUnknown, errors.New(msg)
|
||||||
|
}
|
||||||
41
pkg/commands/socket_detection_windows.go
Normal file
41
pkg/commands/socket_detection_windows.go
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DockerSocketSchema = "npipe://"
|
||||||
|
DockerSocketPath = "//./pipe/docker_engine"
|
||||||
|
|
||||||
|
defaultDockerHost = DockerSocketSchema + DockerSocketPath
|
||||||
|
)
|
||||||
|
|
||||||
|
func detectPlatformCandidates(log *logrus.Entry) (string, ContainerRuntime, error) {
|
||||||
|
// Try Docker Desktop first
|
||||||
|
dockerHost := defaultDockerHost
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout)
|
||||||
|
err := validateSocket(ctx, dockerHost, false)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return dockerHost, RuntimeDocker, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Podman on Windows
|
||||||
|
podmanHost := "npipe:////./pipe/podman-machine-default"
|
||||||
|
ctx, cancel = context.WithTimeout(context.Background(), socketValidationTimeout)
|
||||||
|
err = validateSocket(ctx, podmanHost, false)
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return podmanHost, RuntimePodman, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to default Docker host
|
||||||
|
return dockerHost, RuntimeDocker, nil
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue