-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Implement proper detection with Podman support and enhance error handling #729
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
DeiAsPie
wants to merge
7
commits into
jesseduffield:master
Choose a base branch
from
DeiAsPie:feature/add-proper-detection-with-podman-support
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 5 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
12a6289
Implement proper detection with Podman support and enhance error hand…
DeiAsPie 0eec22e
docs: add Podman and alternative runtime support to README
DeiAsPie cb517d9
Implement proper detection with Podman support and enhance error hand…
DeiAsPie 81e4e44
Enhance Podman support with improved socket detection and error handl…
DeiAsPie 22b68c5
Implement proper detection and support for Podman, enhancing socket v…
DeiAsPie 886b793
Enhance Podman support with improved socket detection and error messa…
DeiAsPie b2e0d44
Refactor socket error handling to unify Docker and Podman socket dete…
DeiAsPie File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,257 @@ | ||
| 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" | ||
| ) | ||
|
|
||
| var ( | ||
| ErrNoDockerSocket = fmt.Errorf("no working Docker/Podman socket found") | ||
| ) | ||
|
|
||
| // Timeout for validating socket connectivity | ||
| const socketValidationTimeout = 3 * time.Second | ||
|
|
||
| var ( | ||
| validateSocketFunc = validateSocket | ||
| inferRuntimeFromHostFunc = inferRuntimeFromHost | ||
| getHostFromContextFunc = getHostFromContext | ||
| detectPlatformCandidatesFunc = detectPlatformCandidates | ||
|
|
||
| // For testing getHostFromContext | ||
| cliconfigLoadFunc = cliconfig.Load | ||
| ctxstoreNewFunc = ctxstore.New | ||
| ) | ||
|
|
||
| // 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 | ||
| dockerHostMu sync.Mutex | ||
| ) | ||
|
|
||
| // DetectDockerHost finds a working Docker/Podman socket | ||
| // Results are cached after first successful detection | ||
| func DetectDockerHost(log *logrus.Entry) (string, ContainerRuntime, error) { | ||
| dockerHostMu.Lock() | ||
| defer dockerHostMu.Unlock() | ||
|
|
||
| if cachedDockerHost != "" { | ||
| return cachedDockerHost, cachedRuntime, nil | ||
| } | ||
|
|
||
| host, runtime, err := detectDockerHostInternal(log) | ||
| if err != nil { | ||
| return "", RuntimeUnknown, err | ||
| } | ||
|
|
||
| cachedDockerHost = host | ||
| cachedRuntime = runtime | ||
| return host, runtime, nil | ||
| } | ||
|
|
||
| // ResetDockerHostCache resets the cached docker host. | ||
| // | ||
| // This is serialized with DetectDockerHost via dockerHostMu. | ||
| // Primarily used for testing. | ||
| func ResetDockerHostCache() { | ||
| dockerHostMu.Lock() | ||
| defer dockerHostMu.Unlock() | ||
| cachedDockerHost = "" | ||
| cachedRuntime = RuntimeUnknown | ||
| } | ||
DeiAsPie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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 != "" { | ||
| log.Debugf("Using DOCKER_HOST from environment: %s", dockerHost) | ||
|
|
||
| // Handle plain paths without schema | ||
| if !strings.Contains(dockerHost, "://") { | ||
| if _, err := os.Stat(dockerHost); err == nil { | ||
| log.Debugf("DOCKER_HOST is a plain path, assuming %s", DockerSocketSchema) | ||
| dockerHost = DockerSocketSchema + dockerHost | ||
| } | ||
| } | ||
|
|
||
DeiAsPie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if !strings.HasPrefix(dockerHost, "ssh://") { | ||
| ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout) | ||
| defer cancel() | ||
| 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 | ||
| } | ||
|
|
||
| // SSH hosts can point to either Docker or Podman; we don't attempt runtime inference here. | ||
| return dockerHost, RuntimeUnknown, nil | ||
| } | ||
|
|
||
| // Priority 2: Docker Context | ||
| contextHost, err := getHostFromContextFunc() | ||
| 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) | ||
| isValid := true | ||
| if !strings.HasPrefix(contextHost, "ssh://") { | ||
| ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout) | ||
| defer cancel() | ||
| if err := validateSocketFunc(ctx, contextHost, false); err != nil { | ||
| if os.Getenv("DOCKER_CONTEXT") != "" { | ||
| return "", RuntimeUnknown, fmt.Errorf("DOCKER_CONTEXT host %s is not accessible: %w", contextHost, err) | ||
| } | ||
| log.Warnf("Context host %s is not accessible: %v", contextHost, err) | ||
| isValid = false | ||
| } | ||
| } | ||
|
|
||
| if isValid { | ||
| 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 | ||
DeiAsPie marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| // Priority 3: Platform-specific candidates | ||
| return detectPlatformCandidatesFunc(log) | ||
| } | ||
|
|
||
| // getHostFromContext retrieves the host from the current Docker context | ||
| func getHostFromContext() (string, error) { | ||
| currentContext := os.Getenv("DOCKER_CONTEXT") | ||
| if currentContext == "" { | ||
| cf, err := cliconfigLoadFunc(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 := ctxstoreNewFunc(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 | ||
| } | ||
DeiAsPie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // 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) { | ||
DeiAsPie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return RuntimePodman, nil | ||
| } | ||
| for _, c := range v.Components { | ||
| if strings.Contains(strings.ToLower(c.Name), needle) { | ||
| return RuntimePodman, nil | ||
| } | ||
| } | ||
|
|
||
| return RuntimeDocker, nil | ||
| } | ||
DeiAsPie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.