mirror of
https://github.com/jesseduffield/lazydocker.git
synced 2026-07-25 08:31:03 +00:00
Merge b2e0d44c6b into 7e7aadc207
This commit is contained in:
commit
3a7d972181
9 changed files with 1293 additions and 78 deletions
27
README.md
27
README.md
|
|
@ -89,6 +89,33 @@ Memorising docker commands is hard. Memorising aliases is slightly less hard. Ke
|
||||||
- Docker >= **29.0.0** (API >= **1.24**)
|
- Docker >= **29.0.0** (API >= **1.24**)
|
||||||
- Docker-Compose >= **1.23.2** (optional)
|
- Docker-Compose >= **1.23.2** (optional)
|
||||||
|
|
||||||
|
## Container Runtime Support
|
||||||
|
|
||||||
|
Lazydocker automatically detects and works with multiple container runtimes:
|
||||||
|
|
||||||
|
- **Docker** (standard, rootless, and Docker Desktop)
|
||||||
|
- **Podman** (rootful and rootless)
|
||||||
|
- **Colima, OrbStack, Lima, Rancher Desktop**
|
||||||
|
|
||||||
|
No manual configuration needed—socket detection is automatic!
|
||||||
|
|
||||||
|
### Using Podman
|
||||||
|
|
||||||
|
If lazydocker can't connect to Podman automatically, enable the Podman socket:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Enable Podman socket service
|
||||||
|
systemctl --user enable --now podman.socket
|
||||||
|
|
||||||
|
# Run lazydocker
|
||||||
|
lazydocker
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, set `DOCKER_HOST`:
|
||||||
|
```sh
|
||||||
|
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"
|
||||||
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Homebrew
|
### Homebrew
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,6 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/imdario/mergo"
|
"github.com/imdario/mergo"
|
||||||
|
|
@ -101,11 +98,14 @@ func newDockerClient(dockerHost string) (*client.Client, error) {
|
||||||
|
|
||||||
// NewDockerCommand it runs docker commands
|
// NewDockerCommand it runs docker commands
|
||||||
func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*DockerCommand, error) {
|
func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*DockerCommand, error) {
|
||||||
dockerHost, err := determineDockerHost()
|
// Use new detection with caching and validation
|
||||||
|
dockerHost, runtime, err := DetectDockerHost(log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ogLog.Printf("> could not determine host %v", err)
|
return nil, fmt.Errorf("cannot connect to container runtime: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Infof("Detected %s runtime at %s", runtime, dockerHost)
|
||||||
|
|
||||||
// NOTE: Inject the determined docker host to the environment. This allows the
|
// NOTE: Inject the determined docker host to the environment. This allows the
|
||||||
// `SSHHandler.HandleSSHDockerHost()` to create a local unix socket tunneled
|
// `SSHHandler.HandleSSHDockerHost()` to create a local unix socket tunneled
|
||||||
// over SSH to the specified ssh host.
|
// over SSH to the specified ssh host.
|
||||||
|
|
@ -522,64 +522,3 @@ func (c *DockerCommand) DockerComposeConfigForProject(project *Project) string {
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
// determineDockerHost tries to the determine the docker host that we should connect to
|
|
||||||
// in the following order of decreasing precedence:
|
|
||||||
// - value of "DOCKER_HOST" environment variable
|
|
||||||
// - host retrieved from the current context (specified via DOCKER_CONTEXT)
|
|
||||||
// - "default docker host" for the host operating system, otherwise
|
|
||||||
func determineDockerHost() (string, error) {
|
|
||||||
// If the docker host is explicitly set via the "DOCKER_HOST" environment variable,
|
|
||||||
// then its a no-brainer :shrug:
|
|
||||||
if os.Getenv("DOCKER_HOST") != "" {
|
|
||||||
return os.Getenv("DOCKER_HOST"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
currentContext := os.Getenv("DOCKER_CONTEXT")
|
|
||||||
if currentContext == "" {
|
|
||||||
cf, err := cliconfig.Load(cliconfig.Dir())
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
currentContext = cf.CurrentContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// On some systems (windows) `default` is stored in the docker config as the currentContext.
|
|
||||||
if currentContext == "" || currentContext == "default" {
|
|
||||||
// If a docker context is neither specified via the "DOCKER_CONTEXT" environment variable nor via the
|
|
||||||
// $HOME/.docker/config file, then we fall back to connecting to the "default docker host" meant for
|
|
||||||
// the host operating system.
|
|
||||||
return defaultDockerHost, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
storeConfig := ctxstore.NewConfig(
|
|
||||||
func() interface{} { return &ddocker.EndpointMeta{} },
|
|
||||||
ctxstore.EndpointTypeGetter(ddocker.DockerEndpoint, func() interface{} { return &ddocker.EndpointMeta{} }),
|
|
||||||
)
|
|
||||||
|
|
||||||
st := ctxstore.New(cliconfig.ContextStoreDir(), storeConfig)
|
|
||||||
md, err := st.GetMetadata(currentContext)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
dockerEP, ok := md.Endpoints[ddocker.DockerEndpoint]
|
|
||||||
if !ok {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
dockerEPMeta, ok := dockerEP.(ddocker.EndpointMeta)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("expected docker.EndpointMeta, got %T", dockerEP)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dockerEPMeta.Host != "" {
|
|
||||||
return dockerEPMeta.Host, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// We might end up here, if the context was created with the `host` set to an empty value (i.e. '').
|
|
||||||
// For example:
|
|
||||||
// ```sh
|
|
||||||
// docker context create foo --docker "host="
|
|
||||||
// ```
|
|
||||||
// In such scenario, we mimic the `docker` cli and try to connect to the "default docker host".
|
|
||||||
return defaultDockerHost, nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
//go:build !windows
|
|
||||||
|
|
||||||
package commands
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultDockerHost = "unix:///var/run/docker.sock"
|
|
||||||
)
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
package commands
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultDockerHost = "npipe:////./pipe/docker_engine"
|
|
||||||
)
|
|
||||||
283
pkg/commands/socket_detection_common.go
Normal file
283
pkg/commands/socket_detection_common.go
Normal file
|
|
@ -0,0 +1,283 @@
|
||||||
|
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 (
|
||||||
|
// ErrNoContainerSocket is returned when no working Docker or Podman socket is found.
|
||||||
|
ErrNoContainerSocket = fmt.Errorf("no working Docker/Podman socket found")
|
||||||
|
// ErrNoDockerSocket is an alias for ErrNoContainerSocket for backwards compatibility.
|
||||||
|
// Deprecated: Use ErrNoContainerSocket instead.
|
||||||
|
ErrNoDockerSocket = ErrNoContainerSocket
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 = func(dir string, config ctxstore.Config) storeInterface {
|
||||||
|
return ctxstore.New(dir, config)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type storeInterface interface {
|
||||||
|
GetMetadata(name string) (ctxstore.Metadata, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func inferRuntimeFromHostHeuristic(host string) ContainerRuntime {
|
||||||
|
lowerHost := strings.ToLower(host)
|
||||||
|
if strings.Contains(lowerHost, "podman") {
|
||||||
|
return RuntimePodman
|
||||||
|
}
|
||||||
|
return RuntimeDocker
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSSHHost returns true if the host uses the SSH protocol.
|
||||||
|
func isSSHHost(host string) bool {
|
||||||
|
return strings.HasPrefix(host, "ssh://")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isSSHHost(dockerHost) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout)
|
||||||
|
defer cancel()
|
||||||
|
if err := validateSocketFunc(ctx, dockerHost, true); err != nil {
|
||||||
|
errMsg := formatConnectionError(ctx, err)
|
||||||
|
return "", RuntimeUnknown, fmt.Errorf("DOCKER_HOST=%s is set but not accessible: %s", dockerHost, errMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if !isSSHHost(contextHost) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout)
|
||||||
|
defer cancel()
|
||||||
|
if err := validateSocketFunc(ctx, contextHost, false); err != nil {
|
||||||
|
errMsg := formatConnectionError(ctx, err)
|
||||||
|
if os.Getenv("DOCKER_CONTEXT") != "" {
|
||||||
|
return "", RuntimeUnknown, fmt.Errorf("DOCKER_CONTEXT host %s is not accessible: %s", contextHost, errMsg)
|
||||||
|
}
|
||||||
|
log.Warnf("Context host %s is not accessible: %s", contextHost, errMsg)
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// SSH hosts can point to either Docker or Podman; we don't attempt runtime inference here.
|
||||||
|
return contextHost, RuntimeUnknown, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 3: Platform-specific candidates
|
||||||
|
return detectPlatformCandidatesFunc(log)
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatConnectionError returns a user-friendly error message, distinguishing timeout from other errors.
|
||||||
|
func formatConnectionError(ctx context.Context, err error) string {
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
return "connection timed out (is the container runtime running?)"
|
||||||
|
}
|
||||||
|
errStr := strings.ToLower(err.Error())
|
||||||
|
if strings.Contains(errStr, "permission denied") || strings.Contains(errStr, "eacces") {
|
||||||
|
return "permission denied (check your user permissions for this socket)"
|
||||||
|
}
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
603
pkg/commands/socket_detection_test.go
Normal file
603
pkg/commands/socket_detection_test.go
Normal file
|
|
@ -0,0 +1,603 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
|
ddocker "github.com/docker/cli/cli/context/docker"
|
||||||
|
ctxstore "github.com/docker/cli/cli/context/store"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetSocketCandidates(t *testing.T) {
|
||||||
|
// Save and restore
|
||||||
|
oldGetuid := getuidFunc
|
||||||
|
oldGetenv := getenvFunc
|
||||||
|
oldUserHomeDir := userHomeDirFunc
|
||||||
|
defer func() {
|
||||||
|
getuidFunc = oldGetuid
|
||||||
|
getenvFunc = oldGetenv
|
||||||
|
userHomeDirFunc = oldUserHomeDir
|
||||||
|
}()
|
||||||
|
|
||||||
|
getuidFunc = func() int { return 1000 }
|
||||||
|
getenvFunc = func(key string) string {
|
||||||
|
if key == "XDG_RUNTIME_DIR" {
|
||||||
|
return "/run/user/1000"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
userHomeDirFunc = func() (string, error) { return "/home/user", nil }
|
||||||
|
|
||||||
|
candidates := getSocketCandidates()
|
||||||
|
|
||||||
|
expectedPaths := []string{
|
||||||
|
"unix:///var/run/docker.sock",
|
||||||
|
"unix:///run/user/1000/docker.sock",
|
||||||
|
"unix:///home/user/.docker/run/docker.sock",
|
||||||
|
"unix:///run/user/1000/podman/podman.sock",
|
||||||
|
"unix:///home/user/.colima/default/docker.sock",
|
||||||
|
"unix:///home/user/.orbstack/run/docker.sock",
|
||||||
|
"unix:///home/user/.lima/default/sock/docker.sock",
|
||||||
|
"unix:///home/user/.rd/docker.sock",
|
||||||
|
}
|
||||||
|
|
||||||
|
var paths []string
|
||||||
|
for _, c := range candidates {
|
||||||
|
paths = append(paths, c.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expected := range expectedPaths {
|
||||||
|
assert.Contains(t, paths, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectDockerHost_DOCKER_HOST_Priority(t *testing.T) {
|
||||||
|
// Save env var
|
||||||
|
oldDockerHost := os.Getenv("DOCKER_HOST")
|
||||||
|
defer os.Setenv("DOCKER_HOST", oldDockerHost)
|
||||||
|
|
||||||
|
expectedHost := "unix:///tmp/custom.sock"
|
||||||
|
os.Setenv("DOCKER_HOST", expectedHost)
|
||||||
|
|
||||||
|
// 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, runtime, err := DetectDockerHost(log)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedHost, host)
|
||||||
|
assert.Equal(t, RuntimePodman, runtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectDockerHost_Caching(t *testing.T) {
|
||||||
|
// Save env var
|
||||||
|
oldDockerHost := os.Getenv("DOCKER_HOST")
|
||||||
|
defer os.Setenv("DOCKER_HOST", oldDockerHost)
|
||||||
|
|
||||||
|
os.Setenv("DOCKER_HOST", "unix:///tmp/first.sock")
|
||||||
|
|
||||||
|
// 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, runtime1, _ := DetectDockerHost(log)
|
||||||
|
|
||||||
|
// Change env var - should still return first one from cache
|
||||||
|
os.Setenv("DOCKER_HOST", "unix:///tmp/second.sock")
|
||||||
|
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) {
|
||||||
|
oldDockerHost := os.Getenv("DOCKER_HOST")
|
||||||
|
os.Setenv("DOCKER_HOST", "unix:///tmp/invalid.sock")
|
||||||
|
defer os.Setenv("DOCKER_HOST", oldDockerHost)
|
||||||
|
|
||||||
|
ResetDockerHostCache()
|
||||||
|
|
||||||
|
oldValidate := validateSocketFunc
|
||||||
|
defer func() { validateSocketFunc = oldValidate }()
|
||||||
|
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
||||||
|
return errors.New("invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
log := logrus.NewEntry(logrus.New())
|
||||||
|
_, _, err := DetectDockerHost(log)
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
assert.Contains(t, err.Error(), "DOCKER_HOST")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectDockerHost_Context_Success(t *testing.T) {
|
||||||
|
oldDockerHost := os.Getenv("DOCKER_HOST")
|
||||||
|
os.Setenv("DOCKER_HOST", "")
|
||||||
|
defer os.Setenv("DOCKER_HOST", oldDockerHost)
|
||||||
|
|
||||||
|
oldGetHost := getHostFromContextFunc
|
||||||
|
oldValidate := validateSocketFunc
|
||||||
|
oldInfer := inferRuntimeFromHostFunc
|
||||||
|
defer func() {
|
||||||
|
getHostFromContextFunc = oldGetHost
|
||||||
|
validateSocketFunc = oldValidate
|
||||||
|
inferRuntimeFromHostFunc = oldInfer
|
||||||
|
}()
|
||||||
|
|
||||||
|
getHostFromContextFunc = func() (string, error) {
|
||||||
|
return "unix:///tmp/context.sock", nil
|
||||||
|
}
|
||||||
|
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
||||||
|
if host == "unix:///tmp/context.sock" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
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, 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) {
|
||||||
|
oldDockerHost := os.Getenv("DOCKER_HOST")
|
||||||
|
os.Setenv("DOCKER_HOST", "")
|
||||||
|
defer os.Setenv("DOCKER_HOST", oldDockerHost)
|
||||||
|
|
||||||
|
oldGetHost := getHostFromContextFunc
|
||||||
|
oldValidate := validateSocketFunc
|
||||||
|
oldDetectPlatform := detectPlatformCandidatesFunc
|
||||||
|
defer func() {
|
||||||
|
getHostFromContextFunc = oldGetHost
|
||||||
|
validateSocketFunc = oldValidate
|
||||||
|
detectPlatformCandidatesFunc = oldDetectPlatform
|
||||||
|
}()
|
||||||
|
|
||||||
|
getHostFromContextFunc = func() (string, error) {
|
||||||
|
return "unix:///tmp/invalid-context.sock", nil
|
||||||
|
}
|
||||||
|
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
||||||
|
return errors.New("invalid")
|
||||||
|
}
|
||||||
|
detectPlatformCandidatesFunc = func(log *logrus.Entry) (string, ContainerRuntime, error) {
|
||||||
|
return "unix:///tmp/fallback.sock", RuntimeDocker, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetDockerHostCache()
|
||||||
|
|
||||||
|
log := logrus.NewEntry(logrus.New())
|
||||||
|
host, _, err := DetectDockerHost(log)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "unix:///tmp/fallback.sock", host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectDockerHost_Context_Invalid(t *testing.T) {
|
||||||
|
// Save env vars
|
||||||
|
oldDockerHost := os.Getenv("DOCKER_HOST")
|
||||||
|
oldDockerContext := os.Getenv("DOCKER_CONTEXT")
|
||||||
|
defer func() {
|
||||||
|
os.Setenv("DOCKER_HOST", oldDockerHost)
|
||||||
|
os.Setenv("DOCKER_CONTEXT", oldDockerContext)
|
||||||
|
}()
|
||||||
|
|
||||||
|
os.Setenv("DOCKER_HOST", "")
|
||||||
|
os.Setenv("DOCKER_CONTEXT", "nonexistent-context-12345")
|
||||||
|
|
||||||
|
// Reset cache for test
|
||||||
|
ResetDockerHostCache()
|
||||||
|
|
||||||
|
log := logrus.NewEntry(logrus.New())
|
||||||
|
_, _, err := DetectDockerHost(log)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "failed to use DOCKER_CONTEXT")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectDockerHost_Context_Error_NoEnv(t *testing.T) {
|
||||||
|
// Save and restore
|
||||||
|
oldGetHost := getHostFromContextFunc
|
||||||
|
oldDetectPlatform := detectPlatformCandidatesFunc
|
||||||
|
defer func() {
|
||||||
|
getHostFromContextFunc = oldGetHost
|
||||||
|
detectPlatformCandidatesFunc = oldDetectPlatform
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Mock getHostFromContext to return error
|
||||||
|
getHostFromContextFunc = func() (string, error) {
|
||||||
|
return "", errors.New("context error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock detectPlatformCandidates to return success so we can see it falls through
|
||||||
|
detectPlatformCandidatesFunc = func(log *logrus.Entry) (string, ContainerRuntime, error) {
|
||||||
|
return "unix:///tmp/fallback.sock", RuntimeDocker, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear DOCKER_HOST
|
||||||
|
oldDockerHost := os.Getenv("DOCKER_HOST")
|
||||||
|
os.Setenv("DOCKER_HOST", "")
|
||||||
|
defer os.Setenv("DOCKER_HOST", oldDockerHost)
|
||||||
|
|
||||||
|
ResetDockerHostCache()
|
||||||
|
|
||||||
|
log := logrus.NewEntry(logrus.New())
|
||||||
|
host, runtime, err := DetectDockerHost(log)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "unix:///tmp/fallback.sock", host)
|
||||||
|
assert.Equal(t, RuntimeDocker, runtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectDockerHost_SSH(t *testing.T) {
|
||||||
|
oldDockerHost := os.Getenv("DOCKER_HOST")
|
||||||
|
os.Setenv("DOCKER_HOST", "ssh://user@host")
|
||||||
|
defer os.Setenv("DOCKER_HOST", oldDockerHost)
|
||||||
|
|
||||||
|
ResetDockerHostCache()
|
||||||
|
|
||||||
|
log := logrus.NewEntry(logrus.New())
|
||||||
|
host, _, err := DetectDockerHost(log)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "ssh://user@host", host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectDockerHost_PlainPath(t *testing.T) {
|
||||||
|
oldDockerHost := os.Getenv("DOCKER_HOST")
|
||||||
|
defer os.Setenv("DOCKER_HOST", oldDockerHost)
|
||||||
|
|
||||||
|
// Create a temporary file to act as a socket
|
||||||
|
tmpFile, err := os.CreateTemp("", "lazydocker-test-*.sock")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer os.Remove(tmpFile.Name())
|
||||||
|
tmpFile.Close()
|
||||||
|
|
||||||
|
os.Setenv("DOCKER_HOST", tmpFile.Name())
|
||||||
|
|
||||||
|
ResetDockerHostCache()
|
||||||
|
|
||||||
|
// Mock validateSocketFunc to succeed
|
||||||
|
oldValidate := validateSocketFunc
|
||||||
|
defer func() { validateSocketFunc = oldValidate }()
|
||||||
|
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log := logrus.NewEntry(logrus.New())
|
||||||
|
host, _, err := DetectDockerHost(log)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "unix://"+tmpFile.Name(), host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInferRuntimeFromHost(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
versionResponse types.Version
|
||||||
|
expectedRuntime ContainerRuntime
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Docker",
|
||||||
|
versionResponse: types.Version{
|
||||||
|
Platform: struct{ Name string }{Name: "Docker Engine - Community"},
|
||||||
|
Components: []types.ComponentVersion{
|
||||||
|
{Name: "Engine", Version: "20.10.7"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedRuntime: RuntimeDocker,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Podman via Platform",
|
||||||
|
versionResponse: types.Version{
|
||||||
|
Platform: struct{ Name string }{Name: "Podman Engine"},
|
||||||
|
},
|
||||||
|
expectedRuntime: RuntimePodman,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Podman via Components",
|
||||||
|
versionResponse: types.Version{
|
||||||
|
Components: []types.ComponentVersion{
|
||||||
|
{Name: "Podman Engine", Version: "3.2.3"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedRuntime: RuntimePodman,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
json.NewEncoder(w).Encode(tt.versionResponse)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
runtime, err := inferRuntimeFromHost(context.Background(), server.URL, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expectedRuntime, runtime)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSocket(t *testing.T) {
|
||||||
|
t.Run("Success", func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
err := validateSocket(context.Background(), server.URL, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Failure", func(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
err := validateSocket(context.Background(), server.URL, false)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHostFromContext(t *testing.T) {
|
||||||
|
oldLoad := cliconfigLoadFunc
|
||||||
|
oldNew := ctxstoreNewFunc
|
||||||
|
defer func() {
|
||||||
|
cliconfigLoadFunc = oldLoad
|
||||||
|
ctxstoreNewFunc = oldNew
|
||||||
|
}()
|
||||||
|
|
||||||
|
t.Run("Default context", func(t *testing.T) {
|
||||||
|
cliconfigLoadFunc = func(dir string) (*configfile.ConfigFile, error) {
|
||||||
|
return &configfile.ConfigFile{CurrentContext: "default"}, nil
|
||||||
|
}
|
||||||
|
host, err := getHostFromContext()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "", host)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Custom context", func(t *testing.T) {
|
||||||
|
cliconfigLoadFunc = func(dir string) (*configfile.ConfigFile, error) {
|
||||||
|
return &configfile.ConfigFile{CurrentContext: "my-context"}, nil
|
||||||
|
}
|
||||||
|
ctxstoreNewFunc = func(dir string, config ctxstore.Config) storeInterface {
|
||||||
|
return &mockStore{
|
||||||
|
metadata: ctxstore.Metadata{
|
||||||
|
Endpoints: map[string]interface{}{
|
||||||
|
ddocker.DockerEndpoint: ddocker.EndpointMeta{
|
||||||
|
Host: "unix:///tmp/my-context.sock",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
host, err := getHostFromContext()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "unix:///tmp/my-context.sock", host)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockStore struct {
|
||||||
|
metadata ctxstore.Metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockStore) GetMetadata(name string) (ctxstore.Metadata, error) {
|
||||||
|
return m.metadata, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectPlatformCandidates_Unix(t *testing.T) {
|
||||||
|
// Save and restore
|
||||||
|
oldStat := statFunc
|
||||||
|
oldValidate := validateSocketFunc
|
||||||
|
defer func() {
|
||||||
|
statFunc = oldStat
|
||||||
|
validateSocketFunc = oldValidate
|
||||||
|
}()
|
||||||
|
|
||||||
|
log := logrus.New().WithField("test", "test")
|
||||||
|
|
||||||
|
t.Run("No sockets exist", func(t *testing.T) {
|
||||||
|
statFunc = func(name string) (os.FileInfo, error) {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
_, _, err := detectPlatformCandidates(log)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "no Docker or Podman socket found")
|
||||||
|
assert.Contains(t, err.Error(), "systemctl --user enable --now podman.socket")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Docker socket exists and is valid", func(t *testing.T) {
|
||||||
|
statFunc = func(name string) (os.FileInfo, error) {
|
||||||
|
if name == "/var/run/docker.sock" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
||||||
|
if host == "unix:///var/run/docker.sock" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("invalid")
|
||||||
|
}
|
||||||
|
host, runtime, err := detectPlatformCandidates(log)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "unix:///var/run/docker.sock", host)
|
||||||
|
assert.Equal(t, RuntimeDocker, runtime)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Docker socket exists but permission denied", func(t *testing.T) {
|
||||||
|
statFunc = func(name string) (os.FileInfo, error) {
|
||||||
|
if name == "/var/run/docker.sock" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
||||||
|
return errors.New("permission denied")
|
||||||
|
}
|
||||||
|
_, _, err := detectPlatformCandidates(log)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "permission denied")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Podman socket exists and is valid", func(t *testing.T) {
|
||||||
|
// Mock getuid to return 1000
|
||||||
|
oldGetuid := getuidFunc
|
||||||
|
defer func() { getuidFunc = oldGetuid }()
|
||||||
|
getuidFunc = func() int { return 1000 }
|
||||||
|
|
||||||
|
statFunc = func(name string) (os.FileInfo, error) {
|
||||||
|
if name == "/run/user/1000/podman/podman.sock" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
||||||
|
if host == "unix:///run/user/1000/podman/podman.sock" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("invalid")
|
||||||
|
}
|
||||||
|
host, runtime, err := detectPlatformCandidates(log)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "unix:///run/user/1000/podman/podman.sock", host)
|
||||||
|
assert.Equal(t, RuntimePodman, runtime)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectPlatformCandidates(t *testing.T) {
|
||||||
|
oldStat := statFunc
|
||||||
|
oldValidate := validateSocketFunc
|
||||||
|
defer func() {
|
||||||
|
statFunc = oldStat
|
||||||
|
validateSocketFunc = oldValidate
|
||||||
|
}()
|
||||||
|
|
||||||
|
log := logrus.NewEntry(logrus.New())
|
||||||
|
|
||||||
|
t.Run("First candidate succeeds", func(t *testing.T) {
|
||||||
|
statFunc = func(name string) (os.FileInfo, error) {
|
||||||
|
return nil, nil // exists
|
||||||
|
}
|
||||||
|
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
host, runtime, err := detectPlatformCandidates(log)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "unix:///var/run/docker.sock", host)
|
||||||
|
assert.Equal(t, RuntimeDocker, runtime)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("First fails, second succeeds", func(t *testing.T) {
|
||||||
|
statFunc = func(name string) (os.FileInfo, error) {
|
||||||
|
if name == "/var/run/docker.sock" {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
host, _, err := detectPlatformCandidates(log)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEqual(t, "unix:///var/run/docker.sock", host)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSocket_UseEnv(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
oldDockerHost := os.Getenv("DOCKER_HOST")
|
||||||
|
os.Setenv("DOCKER_HOST", "unix:///tmp/test.sock")
|
||||||
|
defer os.Setenv("DOCKER_HOST", oldDockerHost)
|
||||||
|
|
||||||
|
// This will fail ping but cover the useEnv branch
|
||||||
|
err := validateSocket(ctx, "unix:///tmp/test.sock", true)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "ping failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHostFromContext_Implementation(t *testing.T) {
|
||||||
|
// Save and restore
|
||||||
|
oldLoad := cliconfigLoadFunc
|
||||||
|
defer func() { cliconfigLoadFunc = oldLoad }()
|
||||||
|
|
||||||
|
t.Run("DOCKER_CONTEXT set", func(t *testing.T) {
|
||||||
|
oldCtx := os.Getenv("DOCKER_CONTEXT")
|
||||||
|
os.Setenv("DOCKER_CONTEXT", "default")
|
||||||
|
defer os.Setenv("DOCKER_CONTEXT", oldCtx)
|
||||||
|
|
||||||
|
host, err := getHostFromContext()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, host)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Config load success but empty context", func(t *testing.T) {
|
||||||
|
cliconfigLoadFunc = func(dir string) (*configfile.ConfigFile, error) {
|
||||||
|
return &configfile.ConfigFile{CurrentContext: ""}, nil
|
||||||
|
}
|
||||||
|
host, err := getHostFromContext()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, host)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Config load error", func(t *testing.T) {
|
||||||
|
cliconfigLoadFunc = func(dir string) (*configfile.ConfigFile, error) {
|
||||||
|
return nil, errors.New("load error")
|
||||||
|
}
|
||||||
|
_, err := getHostFromContext()
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "load error")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSocket_Failures(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Test non-existent path
|
||||||
|
err := validateSocket(ctx, "unix:///tmp/nonexistent-12345.sock", false)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Test invalid schema
|
||||||
|
err = validateSocket(ctx, "invalid:///tmp/test.sock", false)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
169
pkg/commands/socket_detection_unix.go
Normal file
169
pkg/commands/socket_detection_unix.go
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
statFunc = os.Stat
|
||||||
|
getuidFunc = os.Getuid
|
||||||
|
getenvFunc = os.Getenv
|
||||||
|
userHomeDirFunc = os.UserHomeDir
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DockerSocketSchema = "unix://"
|
||||||
|
DockerSocketPath = "/var/run/docker.sock"
|
||||||
|
|
||||||
|
defaultDockerHost = DockerSocketSchema + DockerSocketPath
|
||||||
|
)
|
||||||
|
|
||||||
|
// SocketCandidate represents a potential socket to try
|
||||||
|
type SocketCandidate struct {
|
||||||
|
Path string
|
||||||
|
Runtime ContainerRuntime
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSocketCandidates returns all possible socket paths in priority order
|
||||||
|
func getSocketCandidates() []SocketCandidate {
|
||||||
|
var candidates []SocketCandidate
|
||||||
|
|
||||||
|
// Helper to add candidate if path is valid
|
||||||
|
addCandidate := func(path string, runtime ContainerRuntime) {
|
||||||
|
if path != "" {
|
||||||
|
candidates = append(candidates, SocketCandidate{
|
||||||
|
Path: DockerSocketSchema + path,
|
||||||
|
Runtime: runtime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Standard Docker daemon socket
|
||||||
|
addCandidate(DockerSocketPath, RuntimeDocker)
|
||||||
|
|
||||||
|
xdgRuntime := getenvFunc("XDG_RUNTIME_DIR")
|
||||||
|
home, _ := userHomeDirFunc()
|
||||||
|
uid := getuidFunc()
|
||||||
|
|
||||||
|
// 2. Rootless Docker: $XDG_RUNTIME_DIR/docker.sock
|
||||||
|
if xdgRuntime != "" {
|
||||||
|
addCandidate(filepath.Join(xdgRuntime, "docker.sock"), RuntimeDocker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Rootless Docker: ~/.docker/run/docker.sock
|
||||||
|
if home != "" {
|
||||||
|
addCandidate(filepath.Join(home, ".docker", "run", "docker.sock"), RuntimeDocker)
|
||||||
|
// 4. Docker Desktop: ~/.docker/desktop/docker.sock
|
||||||
|
addCandidate(filepath.Join(home, ".docker", "desktop", "docker.sock"), RuntimeDocker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Rootless Docker: /run/user/$UID/docker.sock
|
||||||
|
addCandidate(filepath.Join("/run", "user", strconv.Itoa(uid), "docker.sock"), RuntimeDocker)
|
||||||
|
|
||||||
|
// 6. Colima
|
||||||
|
if home != "" {
|
||||||
|
addCandidate(filepath.Join(home, ".colima", "default", "docker.sock"), RuntimeDocker)
|
||||||
|
addCandidate(filepath.Join(home, ".colima", "docker.sock"), RuntimeDocker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. OrbStack
|
||||||
|
if home != "" {
|
||||||
|
addCandidate(filepath.Join(home, ".orbstack", "run", "docker.sock"), RuntimeDocker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Lima
|
||||||
|
if home != "" {
|
||||||
|
addCandidate(filepath.Join(home, ".lima", "default", "sock", "docker.sock"), RuntimeDocker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. Rancher Desktop
|
||||||
|
if home != "" {
|
||||||
|
addCandidate(filepath.Join(home, ".rd", "docker.sock"), RuntimeDocker)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. Snap Docker
|
||||||
|
addCandidate("/var/snap/docker/current/run/docker.sock", RuntimeDocker)
|
||||||
|
|
||||||
|
// 11. Rootless Podman: $XDG_RUNTIME_DIR/podman/podman.sock
|
||||||
|
if xdgRuntime != "" {
|
||||||
|
addCandidate(filepath.Join(xdgRuntime, "podman", "podman.sock"), RuntimePodman)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12. Rootless Podman: /run/user/$UID/podman/podman.sock
|
||||||
|
addCandidate(filepath.Join("/run", "user", strconv.Itoa(uid), "podman", "podman.sock"), RuntimePodman)
|
||||||
|
|
||||||
|
// 13. Rootless Podman: ~/.local/share/containers/podman/podman.sock
|
||||||
|
if home != "" {
|
||||||
|
addCandidate(filepath.Join(home, ".local", "share", "containers", "podman", "podman.sock"), RuntimePodman)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 14. Rootful Podman: /run/podman/podman.sock
|
||||||
|
addCandidate("/run/podman/podman.sock", RuntimePodman)
|
||||||
|
|
||||||
|
// 15. Podman on macOS (default machine)
|
||||||
|
if home != "" {
|
||||||
|
addCandidate(filepath.Join(home, ".local", "share", "containers", "podman", "machine", "podman-machine-default", "podman.sock"), RuntimePodman)
|
||||||
|
addCandidate(filepath.Join(home, ".local", "share", "containers", "podman", "machine", "qemu", "podman.sock"), RuntimePodman)
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectPlatformCandidates(log *logrus.Entry) (string, ContainerRuntime, error) {
|
||||||
|
var lastErr error
|
||||||
|
candidates := getSocketCandidates()
|
||||||
|
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
socketPath := strings.TrimPrefix(candidate.Path, DockerSocketSchema)
|
||||||
|
|
||||||
|
// Fast path: check if socket file exists
|
||||||
|
if _, err := statFunc(socketPath); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate by actually connecting
|
||||||
|
err := func() error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout)
|
||||||
|
defer cancel()
|
||||||
|
return validateSocketFunc(ctx, candidate.Path, false)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
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 (check your user permissions for this socket)", candidate.Path)
|
||||||
|
} else {
|
||||||
|
lastErr = fmt.Errorf("%s: %v", candidate.Path, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Connected to %s runtime via %s", candidate.Runtime, candidate.Path)
|
||||||
|
return candidate.Path, candidate.Runtime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// All candidates failed - provide actionable error
|
||||||
|
if lastErr != nil {
|
||||||
|
return "", RuntimeUnknown, fmt.Errorf("%w: %v", ErrNoContainerSocket, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var checkedSockets []string
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
checkedSockets = append(checkedSockets, strings.TrimPrefix(candidate.Path, DockerSocketSchema))
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", RuntimeUnknown, fmt.Errorf("%w: no Docker or Podman socket found; checked %s. "+
|
||||||
|
"If you are using Podman with systemd socket activation, try running "+
|
||||||
|
"'systemctl --user enable --now podman.socket' and then re-run this command.",
|
||||||
|
ErrNoContainerSocket, strings.Join(checkedSockets, ", "))
|
||||||
|
}
|
||||||
77
pkg/commands/socket_detection_windows.go
Normal file
77
pkg/commands/socket_detection_windows.go
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DockerSocketSchema = "npipe://"
|
||||||
|
DockerSocketPath = "//./pipe/docker_engine"
|
||||||
|
|
||||||
|
defaultDockerHost = DockerSocketSchema + DockerSocketPath
|
||||||
|
)
|
||||||
|
|
||||||
|
func getPodmanPipes(log *logrus.Entry) []string {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Failed to get user home directory: %v", err)
|
||||||
|
return []string{"npipe:////./pipe/podman-machine-default"}
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir := filepath.Join(home, ".config", "containers", "podman", "machine", "wsl")
|
||||||
|
files, err := os.ReadDir(configDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Failed to read Podman machine config directory %s: %v", configDir, err)
|
||||||
|
return []string{"npipe:////./pipe/podman-machine-default"}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pipes []string
|
||||||
|
for _, f := range files {
|
||||||
|
if !f.IsDir() && filepath.Ext(f.Name()) == ".json" {
|
||||||
|
name := strings.TrimSuffix(f.Name(), ".json")
|
||||||
|
pipes = append(pipes, "npipe:////./pipe/"+name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pipes) == 0 {
|
||||||
|
log.Debug("No Podman machine config files found, falling back to default")
|
||||||
|
return []string{"npipe:////./pipe/podman-machine-default"}
|
||||||
|
}
|
||||||
|
return pipes
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectPlatformCandidates(log *logrus.Entry) (string, ContainerRuntime, error) {
|
||||||
|
// Try Docker Desktop first
|
||||||
|
dockerHost := defaultDockerHost
|
||||||
|
err := func() error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout)
|
||||||
|
defer cancel()
|
||||||
|
return validateSocketFunc(ctx, dockerHost, false)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return dockerHost, RuntimeDocker, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Podman machines on Windows
|
||||||
|
for _, podmanHost := range getPodmanPipes(log) {
|
||||||
|
err = func() error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), socketValidationTimeout)
|
||||||
|
defer cancel()
|
||||||
|
return validateSocketFunc(ctx, podmanHost, false)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return podmanHost, RuntimePodman, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", RuntimeUnknown, ErrNoContainerSocket
|
||||||
|
}
|
||||||
129
pkg/commands/socket_detection_windows_test.go
Normal file
129
pkg/commands/socket_detection_windows_test.go
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDetectPlatformCandidates_Windows_DockerSuccess(t *testing.T) {
|
||||||
|
oldValidate := validateSocketFunc
|
||||||
|
defer func() { validateSocketFunc = oldValidate }()
|
||||||
|
|
||||||
|
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
||||||
|
if host == "npipe:////./pipe/docker_engine" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("mock failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
log := logrus.NewEntry(logrus.New())
|
||||||
|
host, runtime, err := detectPlatformCandidates(log)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "npipe:////./pipe/docker_engine", host)
|
||||||
|
assert.Equal(t, RuntimeDocker, runtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectPlatformCandidates_Windows_PodmanSuccess(t *testing.T) {
|
||||||
|
oldValidate := validateSocketFunc
|
||||||
|
defer func() { validateSocketFunc = oldValidate }()
|
||||||
|
|
||||||
|
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
||||||
|
if host == "npipe:////./pipe/podman-machine-default" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("mock failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
log := logrus.NewEntry(logrus.New())
|
||||||
|
host, runtime, err := detectPlatformCandidates(log)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "npipe:////./pipe/podman-machine-default", host)
|
||||||
|
assert.Equal(t, RuntimePodman, runtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectPlatformCandidates_Windows_Failure(t *testing.T) {
|
||||||
|
oldValidate := validateSocketFunc
|
||||||
|
defer func() { validateSocketFunc = oldValidate }()
|
||||||
|
|
||||||
|
validateSocketFunc = func(ctx context.Context, host string, useEnv bool) error {
|
||||||
|
return errors.New("mock failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
log := logrus.NewEntry(logrus.New())
|
||||||
|
_, _, err := detectPlatformCandidates(log)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, ErrNoContainerSocket)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPodmanPipes(t *testing.T) {
|
||||||
|
log := logrus.NewEntry(logrus.New())
|
||||||
|
|
||||||
|
// Test fallback when home dir is missing or empty
|
||||||
|
// We can't easily mock UserHomeDir without refactoring, but we can test the logic
|
||||||
|
// by creating the expected directory structure in a temp dir and setting USERPROFILE.
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp("", "podman-test")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := os.RemoveAll(tmpDir); err != nil {
|
||||||
|
t.Errorf("failed to remove temp dir %s: %v", tmpDir, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set USERPROFILE to our temp dir
|
||||||
|
oldProfile := os.Getenv("USERPROFILE")
|
||||||
|
t.Cleanup(func() { os.Setenv("USERPROFILE", oldProfile) })
|
||||||
|
os.Setenv("USERPROFILE", tmpDir)
|
||||||
|
|
||||||
|
// 1. Test fallback when directory doesn't exist
|
||||||
|
pipes := getPodmanPipes(log)
|
||||||
|
assert.Equal(t, []string{"npipe:////./pipe/podman-machine-default"}, pipes)
|
||||||
|
|
||||||
|
// 2. Test with actual config files
|
||||||
|
configDir := filepath.Join(tmpDir, ".config", "containers", "podman", "machine", "wsl")
|
||||||
|
err = os.MkdirAll(configDir, 0755)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.WriteFile(filepath.Join(configDir, "machine1.json"), []byte("{}"), 0644)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.WriteFile(filepath.Join(configDir, "machine2.json"), []byte("{}"), 0644)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.WriteFile(filepath.Join(configDir, "not-a-config.txt"), []byte("{}"), 0644)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
pipes = getPodmanPipes(log)
|
||||||
|
assert.Len(t, pipes, 2)
|
||||||
|
assert.Contains(t, pipes, "npipe:////./pipe/machine1")
|
||||||
|
assert.Contains(t, pipes, "npipe:////./pipe/machine2")
|
||||||
|
|
||||||
|
// 3. Test with empty config directory (no JSON files)
|
||||||
|
emptyDir := filepath.Join(tmpDir, ".config", "containers", "podman", "machine", "empty-wsl")
|
||||||
|
err = os.MkdirAll(emptyDir, 0755)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a new temp dir for this specific test
|
||||||
|
tmpDir2, err := os.MkdirTemp("", "podman-test-empty")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := os.RemoveAll(tmpDir2); err != nil {
|
||||||
|
t.Errorf("failed to remove temp dir %s: %v", tmpDir2, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
os.Setenv("USERPROFILE", tmpDir2)
|
||||||
|
|
||||||
|
// Create the wsl directory but leave it empty
|
||||||
|
emptyConfigDir := filepath.Join(tmpDir2, ".config", "containers", "podman", "machine", "wsl")
|
||||||
|
err = os.MkdirAll(emptyConfigDir, 0755)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
pipes = getPodmanPipes(log)
|
||||||
|
assert.Equal(t, []string{"npipe:////./pipe/podman-machine-default"}, pipes)
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue