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:
DeiAsPie 2025-12-29 12:33:33 +05:30
parent 81e4e44309
commit 22b68c552e
No known key found for this signature in database
3 changed files with 93 additions and 9 deletions

View file

@ -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
}

View file

@ -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) {

View file

@ -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)
}