From 22b68c552e82f6eb4d9a631feffefb6972d9a071 Mon Sep 17 00:00:00 2001 From: DeiAsPie <93835541+DeiAsPie@users.noreply.github.com> Date: Mon, 29 Dec 2025 12:33:33 +0530 Subject: [PATCH] Implement proper detection and support for Podman, enhancing socket validation and error handling Signed-off-by: DeiAsPie <93835541+DeiAsPie@users.noreply.github.com> --- pkg/commands/socket_detection_common.go | 71 +++++++++++++++++++++++-- pkg/commands/socket_detection_test.go | 27 ++++++++-- pkg/commands/socket_detection_unix.go | 4 +- 3 files changed, 93 insertions(+), 9 deletions(-) diff --git a/pkg/commands/socket_detection_common.go b/pkg/commands/socket_detection_common.go index 9b885b3e..c1993e78 100644 --- a/pkg/commands/socket_detection_common.go +++ b/pkg/commands/socket_detection_common.go @@ -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 +} diff --git a/pkg/commands/socket_detection_test.go b/pkg/commands/socket_detection_test.go index cc750aa0..e45d2499 100644 --- a/pkg/commands/socket_detection_test.go +++ b/pkg/commands/socket_detection_test.go @@ -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) { diff --git a/pkg/commands/socket_detection_unix.go b/pkg/commands/socket_detection_unix.go index 5bbc2fd7..75a54925 100644 --- a/pkg/commands/socket_detection_unix.go +++ b/pkg/commands/socket_detection_unix.go @@ -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) }