Work around Docker Desktop's macOS socket location change

Docker Desktop 4.13.0+ moved the socket from /var/run/docker.sock to
~/.docker/run/docker.sock on macOS, but the Docker client library still
defaults to the old location for all Unix systems.

Add platform-specific logic to use the correct default on macOS when
--daemon-host is not specified.

This makes skopeo work more reliable out of the box for new users.

Signed-off-by: Jacques Nadeau <jacques@apache.org>
This commit is contained in:
Jacques Nadeau 2025-07-16 11:34:45 -10:00
parent 729a053464
commit 8ec3080e14
3 changed files with 89 additions and 17 deletions

View File

@ -6,6 +6,8 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath"
"runtime"
"strings" "strings"
"time" "time"
@ -32,7 +34,20 @@ import (
"golang.org/x/term" "golang.org/x/term"
) )
// errorShouldDisplayUsage is a subtype of error used by command handlers to indicate that the commands help should be included. // getDefaultDockerSocket returns the platform-specific default Docker socket path
func getDefaultDockerSocket() string {
if runtime.GOOS == "darwin" {
// On macOS, use the user-specific Docker socket location
if home := os.Getenv("HOME"); home != "" {
return "unix://" + filepath.Join(home, ".docker/run/docker.sock")
}
}
// For all other platforms or if HOME is not set, return empty string
// to use the default from the Docker client library
return ""
}
// errorShouldDisplayUsage is a subtype of error used by command handlers to indicate that the command's help should be included.
type errorShouldDisplayUsage struct { type errorShouldDisplayUsage struct {
error error
} }
@ -204,6 +219,12 @@ func (opts *imageOptions) newSystemContext() (*types.SystemContext, error) {
ctx.OCISharedBlobDirPath = opts.sharedBlobDir ctx.OCISharedBlobDirPath = opts.sharedBlobDir
ctx.AuthFilePath = opts.shared.authFilePath ctx.AuthFilePath = opts.shared.authFilePath
ctx.DockerDaemonHost = opts.dockerDaemonHost ctx.DockerDaemonHost = opts.dockerDaemonHost
// Set platform-specific default for Docker daemon host if not explicitly set
if ctx.DockerDaemonHost == "" {
if defaultSocket := getDefaultDockerSocket(); defaultSocket != "" {
ctx.DockerDaemonHost = defaultSocket
}
}
ctx.DockerDaemonCertPath = opts.dockerCertPath ctx.DockerDaemonCertPath = opts.dockerCertPath
if opts.authFilePath.Present() { if opts.authFilePath.Present() {
ctx.AuthFilePath = opts.authFilePath.Value() ctx.AuthFilePath = opts.authFilePath.Value()

View File

@ -0,0 +1,33 @@
//go:build darwin
package main
import (
"os"
"path/filepath"
"testing"
)
func TestGetDefaultDockerSocketDarwin(t *testing.T) {
// Save original HOME
originalHome := os.Getenv("HOME")
defer func() {
os.Setenv("HOME", originalHome)
}()
// Test with HOME set
testHome := "/Users/test"
os.Setenv("HOME", testHome)
expected := "unix://" + filepath.Join(testHome, ".docker/run/docker.sock")
result := getDefaultDockerSocket()
if result != expected {
t.Errorf("Expected %s, got %s", expected, result)
}
// Test with HOME unset
os.Unsetenv("HOME")
result = getDefaultDockerSocket()
if result != "" {
t.Errorf("Expected empty string when HOME is unset, got %s", result)
}
}

View File

@ -4,6 +4,8 @@ import (
"bytes" "bytes"
"errors" "errors"
"os" "os"
"path/filepath"
"runtime"
"testing" "testing"
"github.com/containers/image/v5/copy" "github.com/containers/image/v5/copy"
@ -16,6 +18,21 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
// newExpectedSystemContext creates a SystemContext with expected defaults for tests,
// accounting for platform-specific behavior like macOS Docker socket location
func newExpectedSystemContext() *types.SystemContext {
ctx := &types.SystemContext{
DockerRegistryUserAgent: defaultUserAgent,
}
// On macOS, DockerDaemonHost gets set to the platform-specific default
if runtime.GOOS == "darwin" {
if home := os.Getenv("HOME"); home != "" {
ctx.DockerDaemonHost = "unix://" + filepath.Join(home, ".docker/run/docker.sock")
}
}
return ctx
}
func TestNoteCloseFailure(t *testing.T) { func TestNoteCloseFailure(t *testing.T) {
const description = "description" const description = "description"
@ -73,9 +90,7 @@ func TestImageOptionsNewSystemContext(t *testing.T) {
opts := fakeImageOptions(t, "dest-", true, []string{}, []string{}) opts := fakeImageOptions(t, "dest-", true, []string{}, []string{})
res, err := opts.newSystemContext() res, err := opts.newSystemContext()
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, &types.SystemContext{ assert.Equal(t, newExpectedSystemContext(), res)
DockerRegistryUserAgent: defaultUserAgent,
}, res)
// Set everything to non-default values. // Set everything to non-default values.
opts = fakeImageOptions(t, "dest-", true, []string{ opts = fakeImageOptions(t, "dest-", true, []string{
@ -148,9 +163,7 @@ func TestImageDestOptionsNewSystemContext(t *testing.T) {
opts := fakeImageDestOptions(t, "dest-", true, []string{}, []string{}) opts := fakeImageDestOptions(t, "dest-", true, []string{}, []string{})
res, err := opts.newSystemContext() res, err := opts.newSystemContext()
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, &types.SystemContext{ assert.Equal(t, newExpectedSystemContext(), res)
DockerRegistryUserAgent: defaultUserAgent,
}, res)
authFile := "/tmp/auth.json" authFile := "/tmp/auth.json"
// Make sure when REGISTRY_AUTH_FILE is set the auth file is used // Make sure when REGISTRY_AUTH_FILE is set the auth file is used
@ -162,10 +175,9 @@ func TestImageDestOptionsNewSystemContext(t *testing.T) {
}) })
res, err = opts.newSystemContext() res, err = opts.newSystemContext()
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, &types.SystemContext{ expected := newExpectedSystemContext()
AuthFilePath: authFile, expected.AuthFilePath = authFile
DockerRegistryUserAgent: defaultUserAgent, assert.Equal(t, expected, res)
}, res)
// Set everything to non-default values. // Set everything to non-default values.
opts = fakeImageDestOptions(t, "dest-", true, []string{ opts = fakeImageDestOptions(t, "dest-", true, []string{
@ -255,10 +267,9 @@ func TestImageOptionsUsernamePassword(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} else { } else {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, &types.SystemContext{ expected := newExpectedSystemContext()
DockerRegistryUserAgent: defaultUserAgent, expected.DockerAuthConfig = command.expectedAuthConfig
DockerAuthConfig: command.expectedAuthConfig, assert.Equal(t, expected, res)
}, res)
} }
} }
} }
@ -508,9 +519,16 @@ func TestImageOptionsAuthfileOverride(t *testing.T) {
res, err := opts.newSystemContext() res, err := opts.newSystemContext()
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, &types.SystemContext{ expected := &types.SystemContext{
AuthFilePath: testCase.expectedAuthfilePath, AuthFilePath: testCase.expectedAuthfilePath,
DockerRegistryUserAgent: defaultUserAgent, DockerRegistryUserAgent: defaultUserAgent,
}, res) }
// On macOS, DockerDaemonHost gets set to the platform-specific default
if runtime.GOOS == "darwin" {
if home := os.Getenv("HOME"); home != "" {
expected.DockerDaemonHost = "unix://" + filepath.Join(home, ".docker/run/docker.sock")
}
}
assert.Equal(t, expected, res)
} }
} }