mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-24 08:01: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 (
|
var (
|
||||||
validateSocketFunc = validateSocket
|
validateSocketFunc = validateSocket
|
||||||
|
inferRuntimeFromHostFunc = inferRuntimeFromHost
|
||||||
getHostFromContextFunc = getHostFromContext
|
getHostFromContextFunc = getHostFromContext
|
||||||
detectPlatformCandidatesFunc = detectPlatformCandidates
|
detectPlatformCandidatesFunc = detectPlatformCandidates
|
||||||
|
|
||||||
|
|
@ -68,7 +69,10 @@ func DetectDockerHost(log *logrus.Entry) (string, ContainerRuntime, error) {
|
||||||
return host, runtime, nil
|
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() {
|
func ResetDockerHostCache() {
|
||||||
dockerHostMu.Lock()
|
dockerHostMu.Lock()
|
||||||
defer dockerHostMu.Unlock()
|
defer dockerHostMu.Unlock()
|
||||||
|
|
@ -76,6 +80,14 @@ func ResetDockerHostCache() {
|
||||||
cachedRuntime = RuntimeUnknown
|
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) {
|
func detectDockerHostInternal(log *logrus.Entry) (string, ContainerRuntime, error) {
|
||||||
// Priority 1: Explicit DOCKER_HOST environment variable
|
// Priority 1: Explicit DOCKER_HOST environment variable
|
||||||
if dockerHost := os.Getenv("DOCKER_HOST"); dockerHost != "" {
|
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 {
|
if err := validateSocketFunc(ctx, dockerHost, true); err != nil {
|
||||||
return "", RuntimeUnknown, fmt.Errorf("DOCKER_HOST=%s is set but not accessible: %w", dockerHost, err)
|
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
|
// Priority 2: Docker Context
|
||||||
|
|
@ -123,7 +144,14 @@ func detectDockerHostInternal(log *logrus.Entry) (string, ContainerRuntime, erro
|
||||||
}
|
}
|
||||||
|
|
||||||
if isValid {
|
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
|
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
|
// Mock validateSocketFunc to succeed
|
||||||
oldValidate := validateSocketFunc
|
oldValidate := validateSocketFunc
|
||||||
|
oldInfer := inferRuntimeFromHostFunc
|
||||||
defer func() { validateSocketFunc = oldValidate }()
|
defer func() { validateSocketFunc = oldValidate }()
|
||||||
|
defer func() { inferRuntimeFromHostFunc = oldInfer }()
|
||||||
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
inferRuntimeFromHostFunc = func(ctx context.Context, host string, useEnv bool) (ContainerRuntime, error) {
|
||||||
|
return RuntimePodman, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Reset cache for test
|
// Reset cache for test
|
||||||
ResetDockerHostCache()
|
ResetDockerHostCache()
|
||||||
|
|
||||||
log := logrus.NewEntry(logrus.New())
|
log := logrus.NewEntry(logrus.New())
|
||||||
host, _, err := DetectDockerHost(log)
|
host, runtime, err := DetectDockerHost(log)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, expectedHost, host)
|
assert.Equal(t, expectedHost, host)
|
||||||
|
assert.Equal(t, RuntimePodman, runtime)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetectDockerHost_Caching(t *testing.T) {
|
func TestDetectDockerHost_Caching(t *testing.T) {
|
||||||
|
|
@ -89,23 +95,30 @@ func TestDetectDockerHost_Caching(t *testing.T) {
|
||||||
|
|
||||||
// Mock validateSocketFunc to succeed
|
// Mock validateSocketFunc to succeed
|
||||||
oldValidate := validateSocketFunc
|
oldValidate := validateSocketFunc
|
||||||
|
oldInfer := inferRuntimeFromHostFunc
|
||||||
defer func() { validateSocketFunc = oldValidate }()
|
defer func() { validateSocketFunc = oldValidate }()
|
||||||
|
defer func() { inferRuntimeFromHostFunc = oldInfer }()
|
||||||
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
inferRuntimeFromHostFunc = func(ctx context.Context, host string, useEnv bool) (ContainerRuntime, error) {
|
||||||
|
return RuntimePodman, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Reset cache for test
|
// Reset cache for test
|
||||||
ResetDockerHostCache()
|
ResetDockerHostCache()
|
||||||
|
|
||||||
log := logrus.NewEntry(logrus.New())
|
log := logrus.NewEntry(logrus.New())
|
||||||
host1, _, _ := DetectDockerHost(log)
|
host1, runtime1, _ := DetectDockerHost(log)
|
||||||
|
|
||||||
// Change env var - should still return first one from cache
|
// Change env var - should still return first one from cache
|
||||||
os.Setenv("DOCKER_HOST", "unix:///tmp/second.sock")
|
os.Setenv("DOCKER_HOST", "unix:///tmp/second.sock")
|
||||||
host2, _, _ := DetectDockerHost(log)
|
host2, runtime2, _ := DetectDockerHost(log)
|
||||||
|
|
||||||
assert.Equal(t, host1, host2)
|
assert.Equal(t, host1, host2)
|
||||||
assert.Equal(t, "unix:///tmp/first.sock", 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) {
|
func TestDetectDockerHost_DOCKER_HOST_Invalid(t *testing.T) {
|
||||||
|
|
@ -135,9 +148,11 @@ func TestDetectDockerHost_Context_Success(t *testing.T) {
|
||||||
|
|
||||||
oldGetHost := getHostFromContextFunc
|
oldGetHost := getHostFromContextFunc
|
||||||
oldValidate := validateSocketFunc
|
oldValidate := validateSocketFunc
|
||||||
|
oldInfer := inferRuntimeFromHostFunc
|
||||||
defer func() {
|
defer func() {
|
||||||
getHostFromContextFunc = oldGetHost
|
getHostFromContextFunc = oldGetHost
|
||||||
validateSocketFunc = oldValidate
|
validateSocketFunc = oldValidate
|
||||||
|
inferRuntimeFromHostFunc = oldInfer
|
||||||
}()
|
}()
|
||||||
|
|
||||||
getHostFromContextFunc = func() (string, error) {
|
getHostFromContextFunc = func() (string, error) {
|
||||||
|
|
@ -149,13 +164,17 @@ func TestDetectDockerHost_Context_Success(t *testing.T) {
|
||||||
}
|
}
|
||||||
return errors.New("invalid")
|
return errors.New("invalid")
|
||||||
}
|
}
|
||||||
|
inferRuntimeFromHostFunc = func(ctx context.Context, host string, useEnv bool) (ContainerRuntime, error) {
|
||||||
|
return RuntimePodman, nil
|
||||||
|
}
|
||||||
|
|
||||||
ResetDockerHostCache()
|
ResetDockerHostCache()
|
||||||
|
|
||||||
log := logrus.NewEntry(logrus.New())
|
log := logrus.NewEntry(logrus.New())
|
||||||
host, _, err := DetectDockerHost(log)
|
host, runtime, err := DetectDockerHost(log)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "unix:///tmp/context.sock", host)
|
assert.Equal(t, "unix:///tmp/context.sock", host)
|
||||||
|
assert.Equal(t, RuntimePodman, runtime)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetectDockerHost_Context_Invalid_Fallback(t *testing.T) {
|
func TestDetectDockerHost_Context_Invalid_Fallback(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,8 @@ func detectPlatformCandidates(log *logrus.Entry) (string, ContainerRuntime, erro
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate by actually connecting
|
// Validate by actually connecting
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout)
|
|
||||||
err := func() error {
|
err := func() error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return validateSocketFunc(ctx, candidate.Path, false)
|
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)
|
log.Debugf("Socket %s exists but validation failed: %v", candidate.Path, err)
|
||||||
errStr := strings.ToLower(err.Error())
|
errStr := strings.ToLower(err.Error())
|
||||||
if strings.Contains(errStr, "permission denied") || strings.Contains(errStr, "eacces") {
|
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 {
|
} else {
|
||||||
lastErr = fmt.Errorf("%s: %v", candidate.Path, err)
|
lastErr = fmt.Errorf("%s: %v", candidate.Path, err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue