mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-22 23:21:03 +00:00
Implement proper detection and support for Podman, enhancing socket validation and error handling
Signed-off-by: DeiAsPie <93835541+DeiAsPie@users.noreply.github.com>
This commit is contained in:
parent
81e4e44309
commit
22b68c552e
3 changed files with 93 additions and 9 deletions
|
|
@ -24,6 +24,7 @@ const socketValidationTimeout = 3 * time.Second
|
|||
|
||||
var (
|
||||
validateSocketFunc = validateSocket
|
||||
inferRuntimeFromHostFunc = inferRuntimeFromHost
|
||||
getHostFromContextFunc = getHostFromContext
|
||||
detectPlatformCandidatesFunc = detectPlatformCandidates
|
||||
|
||||
|
|
@ -68,7 +69,10 @@ func DetectDockerHost(log *logrus.Entry) (string, ContainerRuntime, error) {
|
|||
return host, runtime, nil
|
||||
}
|
||||
|
||||
// ResetDockerHostCache resets the cached docker host. Used for testing.
|
||||
// ResetDockerHostCache resets the cached docker host.
|
||||
//
|
||||
// This is serialized with DetectDockerHost via dockerHostMu.
|
||||
// Primarily used for testing.
|
||||
func ResetDockerHostCache() {
|
||||
dockerHostMu.Lock()
|
||||
defer dockerHostMu.Unlock()
|
||||
|
|
@ -76,6 +80,14 @@ func ResetDockerHostCache() {
|
|||
cachedRuntime = RuntimeUnknown
|
||||
}
|
||||
|
||||
func inferRuntimeFromHostHeuristic(host string) ContainerRuntime {
|
||||
lowerHost := strings.ToLower(host)
|
||||
if strings.Contains(lowerHost, "podman") {
|
||||
return RuntimePodman
|
||||
}
|
||||
return RuntimeDocker
|
||||
}
|
||||
|
||||
func detectDockerHostInternal(log *logrus.Entry) (string, ContainerRuntime, error) {
|
||||
// Priority 1: Explicit DOCKER_HOST environment variable
|
||||
if dockerHost := os.Getenv("DOCKER_HOST"); dockerHost != "" {
|
||||
|
|
@ -95,8 +107,17 @@ func detectDockerHostInternal(log *logrus.Entry) (string, ContainerRuntime, erro
|
|||
if err := validateSocketFunc(ctx, dockerHost, true); err != nil {
|
||||
return "", RuntimeUnknown, fmt.Errorf("DOCKER_HOST=%s is set but not accessible: %w", dockerHost, err)
|
||||
}
|
||||
|
||||
runtime, err := inferRuntimeFromHostFunc(ctx, dockerHost, true)
|
||||
if err != nil {
|
||||
log.Debugf("Failed to infer runtime for DOCKER_HOST=%s: %v", dockerHost, err)
|
||||
runtime = inferRuntimeFromHostHeuristic(dockerHost)
|
||||
}
|
||||
return dockerHost, runtime, nil
|
||||
}
|
||||
return dockerHost, RuntimeDocker, nil
|
||||
|
||||
// SSH hosts can point to either Docker or Podman; we don't attempt runtime inference here.
|
||||
return dockerHost, RuntimeUnknown, nil
|
||||
}
|
||||
|
||||
// Priority 2: Docker Context
|
||||
|
|
@ -123,7 +144,14 @@ func detectDockerHostInternal(log *logrus.Entry) (string, ContainerRuntime, erro
|
|||
}
|
||||
|
||||
if isValid {
|
||||
return contextHost, RuntimeDocker, nil
|
||||
ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout)
|
||||
defer cancel()
|
||||
runtime, err := inferRuntimeFromHostFunc(ctx, contextHost, false)
|
||||
if err != nil {
|
||||
log.Debugf("Failed to infer runtime for Docker context host %s: %v", contextHost, err)
|
||||
runtime = inferRuntimeFromHostHeuristic(contextHost)
|
||||
}
|
||||
return contextHost, runtime, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -190,3 +218,40 @@ func validateSocket(ctx context.Context, host string, useEnv bool) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// inferRuntimeFromHost inspects the engine behind the host to distinguish Docker vs Podman.
|
||||
//
|
||||
// It uses the Docker-compatible API. Podman supports this and typically reports itself
|
||||
// via version metadata.
|
||||
func inferRuntimeFromHost(ctx context.Context, host string, useEnv bool) (ContainerRuntime, error) {
|
||||
var opts []client.Opt
|
||||
if useEnv {
|
||||
// If we're validating/inferencing 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 RuntimeUnknown, fmt.Errorf("create client: %w", err)
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
v, err := cli.ServerVersion(ctx)
|
||||
if err != nil {
|
||||
return RuntimeUnknown, fmt.Errorf("server version: %w", err)
|
||||
}
|
||||
|
||||
// Heuristics: Podman typically identifies itself in platform name or components.
|
||||
needle := "podman"
|
||||
if strings.Contains(strings.ToLower(v.Platform.Name), needle) {
|
||||
return RuntimePodman, nil
|
||||
}
|
||||
for _, c := range v.Components {
|
||||
if strings.Contains(strings.ToLower(c.Name), needle) {
|
||||
return RuntimePodman, nil
|
||||
}
|
||||
}
|
||||
|
||||
return RuntimeDocker, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,18 +66,24 @@ func TestDetectDockerHost_DOCKER_HOST_Priority(t *testing.T) {
|
|||
|
||||
// Mock validateSocketFunc to succeed
|
||||
oldValidate := validateSocketFunc
|
||||
oldInfer := inferRuntimeFromHostFunc
|
||||
defer func() { validateSocketFunc = oldValidate }()
|
||||
defer func() { inferRuntimeFromHostFunc = oldInfer }()
|
||||
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
||||
return nil
|
||||
}
|
||||
inferRuntimeFromHostFunc = func(ctx context.Context, host string, useEnv bool) (ContainerRuntime, error) {
|
||||
return RuntimePodman, nil
|
||||
}
|
||||
|
||||
// Reset cache for test
|
||||
ResetDockerHostCache()
|
||||
|
||||
log := logrus.NewEntry(logrus.New())
|
||||
host, _, err := DetectDockerHost(log)
|
||||
host, runtime, err := DetectDockerHost(log)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedHost, host)
|
||||
assert.Equal(t, RuntimePodman, runtime)
|
||||
}
|
||||
|
||||
func TestDetectDockerHost_Caching(t *testing.T) {
|
||||
|
|
@ -89,23 +95,30 @@ func TestDetectDockerHost_Caching(t *testing.T) {
|
|||
|
||||
// Mock validateSocketFunc to succeed
|
||||
oldValidate := validateSocketFunc
|
||||
oldInfer := inferRuntimeFromHostFunc
|
||||
defer func() { validateSocketFunc = oldValidate }()
|
||||
defer func() { inferRuntimeFromHostFunc = oldInfer }()
|
||||
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
||||
return nil
|
||||
}
|
||||
inferRuntimeFromHostFunc = func(ctx context.Context, host string, useEnv bool) (ContainerRuntime, error) {
|
||||
return RuntimePodman, nil
|
||||
}
|
||||
|
||||
// Reset cache for test
|
||||
ResetDockerHostCache()
|
||||
|
||||
log := logrus.NewEntry(logrus.New())
|
||||
host1, _, _ := DetectDockerHost(log)
|
||||
host1, runtime1, _ := DetectDockerHost(log)
|
||||
|
||||
// Change env var - should still return first one from cache
|
||||
os.Setenv("DOCKER_HOST", "unix:///tmp/second.sock")
|
||||
host2, _, _ := DetectDockerHost(log)
|
||||
host2, runtime2, _ := DetectDockerHost(log)
|
||||
|
||||
assert.Equal(t, host1, host2)
|
||||
assert.Equal(t, "unix:///tmp/first.sock", host2)
|
||||
assert.Equal(t, runtime1, runtime2)
|
||||
assert.Equal(t, RuntimePodman, runtime2)
|
||||
}
|
||||
|
||||
func TestDetectDockerHost_DOCKER_HOST_Invalid(t *testing.T) {
|
||||
|
|
@ -135,9 +148,11 @@ func TestDetectDockerHost_Context_Success(t *testing.T) {
|
|||
|
||||
oldGetHost := getHostFromContextFunc
|
||||
oldValidate := validateSocketFunc
|
||||
oldInfer := inferRuntimeFromHostFunc
|
||||
defer func() {
|
||||
getHostFromContextFunc = oldGetHost
|
||||
validateSocketFunc = oldValidate
|
||||
inferRuntimeFromHostFunc = oldInfer
|
||||
}()
|
||||
|
||||
getHostFromContextFunc = func() (string, error) {
|
||||
|
|
@ -149,13 +164,17 @@ func TestDetectDockerHost_Context_Success(t *testing.T) {
|
|||
}
|
||||
return errors.New("invalid")
|
||||
}
|
||||
inferRuntimeFromHostFunc = func(ctx context.Context, host string, useEnv bool) (ContainerRuntime, error) {
|
||||
return RuntimePodman, nil
|
||||
}
|
||||
|
||||
ResetDockerHostCache()
|
||||
|
||||
log := logrus.NewEntry(logrus.New())
|
||||
host, _, err := DetectDockerHost(log)
|
||||
host, runtime, err := DetectDockerHost(log)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "unix:///tmp/context.sock", host)
|
||||
assert.Equal(t, RuntimePodman, runtime)
|
||||
}
|
||||
|
||||
func TestDetectDockerHost_Context_Invalid_Fallback(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -132,8 +132,8 @@ func detectPlatformCandidates(log *logrus.Entry) (string, ContainerRuntime, erro
|
|||
}
|
||||
|
||||
// Validate by actually connecting
|
||||
ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout)
|
||||
err := func() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout)
|
||||
defer cancel()
|
||||
return validateSocketFunc(ctx, candidate.Path, false)
|
||||
}()
|
||||
|
|
@ -142,7 +142,7 @@ func detectPlatformCandidates(log *logrus.Entry) (string, ContainerRuntime, erro
|
|||
log.Debugf("Socket %s exists but validation failed: %v", candidate.Path, err)
|
||||
errStr := strings.ToLower(err.Error())
|
||||
if strings.Contains(errStr, "permission denied") || strings.Contains(errStr, "eacces") {
|
||||
lastErr = fmt.Errorf("%s: permission denied (are you in the %v group?)", candidate.Path, candidate.Runtime)
|
||||
lastErr = fmt.Errorf("%s: permission denied (check your user permissions for this socket)", candidate.Path)
|
||||
} else {
|
||||
lastErr = fmt.Errorf("%s: %v", candidate.Path, err)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue