Browse code

Merge pull request #51910 from thaJeztah/refactor_listener_defaults

loadDaemonCliConfig: explicitly set default host

Sebastiaan van Stijn authored on 2026/01/27 02:16:02
Showing 3 changed files
... ...
@@ -10,7 +10,7 @@ import (
10 10
 	"os"
11 11
 	"path/filepath"
12 12
 	"runtime"
13
-	"sort"
13
+	"slices"
14 14
 	"strings"
15 15
 	"sync"
16 16
 	"time"
... ...
@@ -639,6 +639,30 @@ func loadDaemonCliConfig(opts *daemonOptions) (*config.Config, error) {
639 639
 		}
640 640
 	}
641 641
 
642
+	// TODO(thaJeztah): consider making empty strings an error. Existing behavior allowed for empty strings to be used as default, even if explicitly set (`dockerd -H ""`).
643
+	conf.Hosts = slices.DeleteFunc(conf.Hosts, func(h string) bool {
644
+		return strings.TrimSpace(h) == ""
645
+	})
646
+	if len(conf.Hosts) == 0 {
647
+		// Set the default host if no hosts are configured.
648
+		// TODO(thaJeztah) can set defaults in config.New() instead?
649
+		if conf.TLS != nil && *conf.TLS {
650
+			// If no host is configured, but the "--tls" flag is set, we
651
+			// default to using a TCP connection instead of a unix-socket
652
+			// or named pipe.
653
+			//
654
+			// See https://github.com/moby/moby/commit/0906195fbbd6f379c163b80f23e4c5a60bcfc5f0
655
+			conf.Hosts = append(conf.Hosts, dopts.DefaultTLSHost)
656
+		} else {
657
+			// Otherwise use the default unix-socket (Linux) or named pipe (Windows).
658
+			h, err := defaultAPISocketPath(honorXDG)
659
+			if err != nil {
660
+				return nil, err
661
+			}
662
+			conf.Hosts = append(conf.Hosts, h)
663
+		}
664
+	}
665
+
642 666
 	if err := normalizeHosts(conf); err != nil {
643 667
 		return nil, err
644 668
 	}
... ...
@@ -718,38 +742,44 @@ func loadDaemonCliConfig(opts *daemonOptions) (*config.Config, error) {
718 718
 	return conf, nil
719 719
 }
720 720
 
721
-// normalizeHosts normalizes the configured config.Hosts and remove duplicates.
721
+// defaultAPISocketPath returns the default path for the Unix socket (Linux)
722
+// or named pipe (Windows).
723
+//
724
+// When running with rootlessKit, XDG dirs should be preferred, and the
725
+// default is to listen on an unprivileged socket in [XDG_RUNTIME_DIR].
726
+//
727
+// [XDG_RUNTIME_DIR]: https://specifications.freedesktop.org/basedir/0.8/#variables
728
+func defaultAPISocketPath(honorXDG bool) (string, error) {
729
+	if honorXDG {
730
+		runtimeDir, err := homedir.GetRuntimeDir()
731
+		if err != nil {
732
+			return "", err
733
+		}
734
+		return "unix://" + filepath.Join(runtimeDir, "docker.sock"), nil
735
+	}
736
+
737
+	// default unix-socket (Linux) or named pipe (Windows).
738
+	return dopts.DefaultHost, nil
739
+}
740
+
741
+// normalizeHosts normalizes the configured config.Hosts and removes duplicates.
722 742
 // It returns an error if it fails to parse a host.
723 743
 func normalizeHosts(cfg *config.Config) error {
724 744
 	if len(cfg.Hosts) == 0 {
725
-		// if no hosts are configured, create a single entry slice, so that the
726
-		// default is used.
727
-		//
728
-		// TODO(thaJeztah) implement a cleaner way for this; this depends on a
729
-		//                 side-effect of how we parse empty/partial hosts.
730
-		cfg.Hosts = make([]string, 1)
731
-	}
732
-	hosts := make([]string, 0, len(cfg.Hosts))
733
-	seen := make(map[string]struct{}, len(cfg.Hosts))
734
-
735
-	useTLS := DefaultTLSValue
736
-	if cfg.TLS != nil {
737
-		useTLS = *cfg.TLS
745
+		return errors.New("no hosts specified")
738 746
 	}
739 747
 
740
-	for _, h := range cfg.Hosts {
741
-		host, err := dopts.ParseHost(useTLS, honorXDG, h)
748
+	hosts := slices.Clone(cfg.Hosts)
749
+	for i, h := range hosts {
750
+		var err error
751
+		hosts[i], err = dopts.ParseDaemonHost(h)
742 752
 		if err != nil {
743 753
 			return err
744 754
 		}
745
-		if _, ok := seen[host]; ok {
746
-			continue
747
-		}
748
-		seen[host] = struct{}{}
749
-		hosts = append(hosts, host)
750 755
 	}
751
-	sort.Strings(hosts)
752
-	cfg.Hosts = hosts
756
+
757
+	slices.Sort(hosts)
758
+	cfg.Hosts = slices.Compact(hosts)
753 759
 	return nil
754 760
 }
755 761
 
... ...
@@ -3,11 +3,9 @@ package opts
3 3
 import (
4 4
 	"net"
5 5
 	"net/url"
6
-	"path/filepath"
7 6
 	"strconv"
8 7
 	"strings"
9 8
 
10
-	"github.com/moby/moby/v2/pkg/homedir"
11 9
 	"github.com/pkg/errors"
12 10
 )
13 11
 
... ...
@@ -36,9 +34,9 @@ const (
36 36
 // ValidateHost validates that the specified string is a valid host and returns it.
37 37
 func ValidateHost(val string) (string, error) {
38 38
 	host := strings.TrimSpace(val)
39
-	// The empty string means default and is not handled by parseDaemonHost
39
+	// The empty string means default and is not handled by ParseDaemonHost
40 40
 	if host != "" {
41
-		_, err := parseDaemonHost(host)
41
+		_, err := ParseDaemonHost(host)
42 42
 		if err != nil {
43 43
 			return val, err
44 44
 		}
... ...
@@ -48,35 +46,9 @@ func ValidateHost(val string) (string, error) {
48 48
 	return val, nil
49 49
 }
50 50
 
51
-// ParseHost and set defaults for a Daemon host string.
52
-// defaultToTLS is preferred over defaultToUnixXDG.
53
-func ParseHost(defaultToTLS, defaultToUnixXDG bool, val string) (string, error) {
54
-	host := strings.TrimSpace(val)
55
-	if host == "" {
56
-		if defaultToTLS {
57
-			host = DefaultTLSHost
58
-		} else if defaultToUnixXDG {
59
-			runtimeDir, err := homedir.GetRuntimeDir()
60
-			if err != nil {
61
-				return "", err
62
-			}
63
-			host = "unix://" + filepath.Join(runtimeDir, "docker.sock")
64
-		} else {
65
-			host = DefaultHost
66
-		}
67
-	} else {
68
-		var err error
69
-		host, err = parseDaemonHost(host)
70
-		if err != nil {
71
-			return val, err
72
-		}
73
-	}
74
-	return host, nil
75
-}
76
-
77
-// parseDaemonHost parses the specified address and returns an address that will be used as the host.
51
+// ParseDaemonHost parses the specified address and returns an address that will be used as the host.
78 52
 // Depending on the address specified, this may return one of the global Default* strings defined in hosts.go.
79
-func parseDaemonHost(address string) (string, error) {
53
+func ParseDaemonHost(address string) (string, error) {
80 54
 	proto, addr, ok := strings.Cut(address, "://")
81 55
 	if !ok && proto != "" {
82 56
 		addr = proto
... ...
@@ -6,8 +6,8 @@ import (
6 6
 	"testing"
7 7
 )
8 8
 
9
-func TestParseHost(t *testing.T) {
10
-	invalid := map[string]string{
9
+func TestParseDockerDaemonHost(t *testing.T) {
10
+	invalids := map[string]string{
11 11
 		"something with spaces": `invalid bind address (something with spaces): parse "tcp://something with spaces": invalid character " " in host name`,
12 12
 		"://":                   `invalid bind address (://): unsupported proto ''`,
13 13
 		"unknown://":            `invalid bind address (unknown://): unsupported proto 'unknown'`,
... ...
@@ -20,57 +20,7 @@ func TestParseHost(t *testing.T) {
20 20
 		"tcp://[::1]:/":         `invalid bind address (tcp://[::1]:/): should not contain a path element`,
21 21
 		"tcp://[::1]:5555/":     `invalid bind address (tcp://[::1]:5555/): should not contain a path element`,
22 22
 		"tcp://[::1]:5555/p":    `invalid bind address (tcp://[::1]:5555/p): should not contain a path element`,
23
-		" tcp://:5555/path ":    `invalid bind address (tcp://:5555/path): should not contain a path element`,
24
-	}
25
-
26
-	valid := map[string]string{
27
-		"":                         DefaultHost,
28
-		" ":                        DefaultHost,
29
-		"  ":                       DefaultHost,
30
-		"fd://":                    "fd://",
31
-		"fd://something":           "fd://something",
32
-		"tcp://host:":              fmt.Sprintf("tcp://host:%d", DefaultHTTPPort),
33
-		"tcp://":                   DefaultTCPHost,
34
-		"tcp://:":                  DefaultTCPHost,
35
-		"tcp://:5555":              fmt.Sprintf("tcp://%s:5555", DefaultHTTPHost), //nolint:nosprintfhostport // sprintf is more readable for this case.
36
-		"tcp://[::1]":              fmt.Sprintf(`tcp://[::1]:%d`, DefaultHTTPPort),
37
-		"tcp://[::1]:":             fmt.Sprintf(`tcp://[::1]:%d`, DefaultHTTPPort),
38
-		"tcp://[::1]:5555":         `tcp://[::1]:5555`,
39
-		"tcp://0.0.0.0:5555":       "tcp://0.0.0.0:5555",
40
-		"tcp://192.168:5555":       "tcp://192.168:5555",
41
-		"tcp://192.168.0.1:5555":   "tcp://192.168.0.1:5555",
42
-		"tcp://0.0.0.0:1234567890": "tcp://0.0.0.0:1234567890", // yeah it's valid :P
43
-		"tcp://docker.com:5555":    "tcp://docker.com:5555",
44
-		"unix://":                  "unix://" + DefaultUnixSocket,
45
-		"unix://path/to/socket":    "unix://path/to/socket",
46
-		"npipe://":                 "npipe://" + DefaultNamedPipe,
47
-		"npipe:////./pipe/foo":     "npipe:////./pipe/foo",
48
-	}
49
-
50
-	for value, expectedError := range invalid {
51
-		t.Run(value, func(t *testing.T) {
52
-			_, err := ParseHost(false, false, value)
53
-			if err == nil || err.Error() != expectedError {
54
-				t.Errorf(`expected error "%s", got "%v"`, expectedError, err)
55
-			}
56
-		})
57
-	}
58 23
 
59
-	for value, expected := range valid {
60
-		t.Run(value, func(t *testing.T) {
61
-			actual, err := ParseHost(false, false, value)
62
-			if err != nil {
63
-				t.Errorf(`unexpected error: "%v"`, err)
64
-			}
65
-			if actual != expected {
66
-				t.Errorf(`expected "%s", got "%s""`, expected, actual)
67
-			}
68
-		})
69
-	}
70
-}
71
-
72
-func TestParseDockerDaemonHost(t *testing.T) {
73
-	invalids := map[string]string{
74 24
 		"tcp:a.b.c.d":                   `invalid bind address (tcp:a.b.c.d): parse "tcp://tcp:a.b.c.d": invalid port ":a.b.c.d" after host`,
75 25
 		"tcp:a.b.c.d/path":              `invalid bind address (tcp:a.b.c.d/path): parse "tcp://tcp:a.b.c.d/path": invalid port ":a.b.c.d" after host`,
76 26
 		"tcp://127.0.0.1/":              "invalid bind address (tcp://127.0.0.1/): should not contain a path element",
... ...
@@ -79,6 +29,7 @@ func TestParseDockerDaemonHost(t *testing.T) {
79 79
 		"tcp://unix:///run/docker.sock": "invalid bind address (tcp://unix:///run/docker.sock): should not contain a path element",
80 80
 		" tcp://:5555/path ":            "invalid bind address ( tcp://:5555/path ): unsupported proto ' tcp'", //nolint:gocritic // This is a valid test case.
81 81
 		"":                              "invalid bind address (): unsupported proto ''",
82
+		" ":                             `invalid bind address ( ): parse "tcp:// ": invalid character " " in host name`,
82 83
 		":5555/path":                    "invalid bind address (:5555/path): should not contain a path element",
83 84
 		"0.0.0.1:5555/path":             "invalid bind address (0.0.0.1:5555/path): should not contain a path element",
84 85
 		"[::1]:5555/path":               "invalid bind address ([::1]:5555/path): should not contain a path element",
... ...
@@ -89,34 +40,42 @@ func TestParseDockerDaemonHost(t *testing.T) {
89 89
 		"unix://unix://tcp://127.0.0.1": "invalid bind address (unix://unix://tcp://127.0.0.1): invalid unix address: unix://tcp://127.0.0.1",
90 90
 	}
91 91
 	valids := map[string]string{
92
-		":":                       DefaultTCPHost,
93
-		":5555":                   fmt.Sprintf("tcp://%s:5555", DefaultHTTPHost), //nolint:nosprintfhostport // sprintf is more readable for this case.
94
-		"0.0.0.1:":                fmt.Sprintf("tcp://0.0.0.1:%d", DefaultHTTPPort),
95
-		"0.0.0.1:5555":            "tcp://0.0.0.1:5555",
96
-		"[::1]":                   fmt.Sprintf("tcp://[::1]:%d", DefaultHTTPPort),
97
-		"[::1]:":                  fmt.Sprintf("tcp://[::1]:%d", DefaultHTTPPort),
98
-		"[::1]:5555":              "tcp://[::1]:5555",
99
-		"[0:0:0:0:0:0:0:1]":       fmt.Sprintf("tcp://[0:0:0:0:0:0:0:1]:%d", DefaultHTTPPort),
100
-		"[0:0:0:0:0:0:0:1]:":      fmt.Sprintf("tcp://[0:0:0:0:0:0:0:1]:%d", DefaultHTTPPort),
101
-		"[0:0:0:0:0:0:0:1]:5555":  "tcp://[0:0:0:0:0:0:0:1]:5555",
102
-		"localhost":               fmt.Sprintf("tcp://localhost:%d", DefaultHTTPPort),
103
-		"localhost:":              fmt.Sprintf("tcp://localhost:%d", DefaultHTTPPort),
104
-		"localhost:5555":          "tcp://localhost:5555",
105
-		"fd://":                   "fd://",
106
-		"fd://something":          "fd://something",
107
-		"npipe://":                "npipe://" + DefaultNamedPipe,
108
-		"npipe:////./pipe/foo":    "npipe:////./pipe/foo",
109
-		"tcp://":                  DefaultTCPHost,
110
-		"tcp://:5555":             fmt.Sprintf("tcp://%s:5555", DefaultHTTPHost), //nolint:nosprintfhostport // sprintf is more readable for this case.
111
-		"tcp://[::1]":             fmt.Sprintf("tcp://[::1]:%d", DefaultHTTPPort),
112
-		"tcp://[::1]:":            fmt.Sprintf("tcp://[::1]:%d", DefaultHTTPPort),
113
-		"tcp://[::1]:5555":        "tcp://[::1]:5555",
114
-		"unix://":                 "unix://" + DefaultUnixSocket,
115
-		"unix:///run/docker.sock": "unix:///run/docker.sock",
92
+		":":                        DefaultTCPHost,
93
+		":5555":                    fmt.Sprintf("tcp://%s:5555", DefaultHTTPHost), //nolint:nosprintfhostport // sprintf is more readable for this case.
94
+		"0.0.0.1:":                 fmt.Sprintf("tcp://0.0.0.1:%d", DefaultHTTPPort),
95
+		"0.0.0.1:5555":             "tcp://0.0.0.1:5555",
96
+		"[::1]":                    fmt.Sprintf("tcp://[::1]:%d", DefaultHTTPPort),
97
+		"[::1]:":                   fmt.Sprintf("tcp://[::1]:%d", DefaultHTTPPort),
98
+		"[::1]:5555":               "tcp://[::1]:5555",
99
+		"[0:0:0:0:0:0:0:1]":        fmt.Sprintf("tcp://[0:0:0:0:0:0:0:1]:%d", DefaultHTTPPort),
100
+		"[0:0:0:0:0:0:0:1]:":       fmt.Sprintf("tcp://[0:0:0:0:0:0:0:1]:%d", DefaultHTTPPort),
101
+		"[0:0:0:0:0:0:0:1]:5555":   "tcp://[0:0:0:0:0:0:0:1]:5555",
102
+		"localhost":                fmt.Sprintf("tcp://localhost:%d", DefaultHTTPPort),
103
+		"localhost:":               fmt.Sprintf("tcp://localhost:%d", DefaultHTTPPort),
104
+		"localhost:5555":           "tcp://localhost:5555",
105
+		"fd://":                    "fd://",
106
+		"fd://something":           "fd://something",
107
+		"npipe://":                 "npipe://" + DefaultNamedPipe,
108
+		"npipe:////./pipe/foo":     "npipe:////./pipe/foo",
109
+		"tcp://host:":              fmt.Sprintf("tcp://host:%d", DefaultHTTPPort),
110
+		"tcp://":                   DefaultTCPHost,
111
+		"tcp://:":                  DefaultTCPHost,
112
+		"tcp://:5555":              fmt.Sprintf("tcp://%s:5555", DefaultHTTPHost), //nolint:nosprintfhostport // sprintf is more readable for this case.
113
+		"tcp://[::1]":              fmt.Sprintf(`tcp://[::1]:%d`, DefaultHTTPPort),
114
+		"tcp://[::1]:":             fmt.Sprintf(`tcp://[::1]:%d`, DefaultHTTPPort),
115
+		"tcp://[::1]:5555":         `tcp://[::1]:5555`,
116
+		"tcp://0.0.0.0:5555":       "tcp://0.0.0.0:5555",
117
+		"tcp://192.168:5555":       "tcp://192.168:5555",
118
+		"tcp://192.168.0.1:5555":   "tcp://192.168.0.1:5555",
119
+		"tcp://0.0.0.0:1234567890": "tcp://0.0.0.0:1234567890", // yeah it's valid :P
120
+		"tcp://docker.com:5555":    "tcp://docker.com:5555",
121
+		"unix://":                  "unix://" + DefaultUnixSocket,
122
+		"unix://path/to/socket":    "unix://path/to/socket",
123
+		"unix:///run/docker.sock":  "unix:///run/docker.sock",
116 124
 	}
117 125
 	for invalidAddr, expectedError := range invalids {
118 126
 		t.Run(invalidAddr, func(t *testing.T) {
119
-			addr, err := parseDaemonHost(invalidAddr)
127
+			addr, err := ParseDaemonHost(invalidAddr)
120 128
 			if err == nil || err.Error() != expectedError {
121 129
 				t.Errorf(`expected error "%s", got "%v"`, expectedError, err)
122 130
 			}
... ...
@@ -127,7 +86,7 @@ func TestParseDockerDaemonHost(t *testing.T) {
127 127
 	}
128 128
 	for validAddr, expectedAddr := range valids {
129 129
 		t.Run(validAddr, func(t *testing.T) {
130
-			addr, err := parseDaemonHost(validAddr)
130
+			addr, err := ParseDaemonHost(validAddr)
131 131
 			if err != nil {
132 132
 				t.Errorf(`unexpected error: "%v"`, err)
133 133
 			}