Browse code

Allow to set daemon and server configurations in a file.

Read configuration after flags making this the priority:

1- Apply configuration from file.
2- Apply configuration from flags.

Reload configuration when a signal is received, USR2 in Linux:

- Reload router if the debug configuration changes.
- Reload daemon labels.
- Reload cluster discovery.

Signed-off-by: David Calavera <david.calavera@gmail.com>

David Calavera authored on 2015/12/11 08:35:10
Showing 23 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,30 @@
0
+package server
1
+
2
+import (
3
+	"net/http"
4
+	"sync"
5
+
6
+	"github.com/gorilla/mux"
7
+)
8
+
9
+// routerSwapper is an http.Handler that allow you to swap
10
+// mux routers.
11
+type routerSwapper struct {
12
+	mu     sync.Mutex
13
+	router *mux.Router
14
+}
15
+
16
+// Swap changes the old router with the new one.
17
+func (rs *routerSwapper) Swap(newRouter *mux.Router) {
18
+	rs.mu.Lock()
19
+	rs.router = newRouter
20
+	rs.mu.Unlock()
21
+}
22
+
23
+// ServeHTTP makes the routerSwapper to implement the http.Handler interface.
24
+func (rs *routerSwapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
25
+	rs.mu.Lock()
26
+	router := rs.router
27
+	rs.mu.Unlock()
28
+	router.ServeHTTP(w, r)
29
+}
... ...
@@ -4,7 +4,6 @@ import (
4 4
 	"crypto/tls"
5 5
 	"net"
6 6
 	"net/http"
7
-	"os"
8 7
 	"strings"
9 8
 
10 9
 	"github.com/Sirupsen/logrus"
... ...
@@ -42,10 +41,11 @@ type Config struct {
42 42
 
43 43
 // Server contains instance details for the server
44 44
 type Server struct {
45
-	cfg          *Config
46
-	servers      []*HTTPServer
47
-	routers      []router.Router
48
-	authZPlugins []authorization.Plugin
45
+	cfg           *Config
46
+	servers       []*HTTPServer
47
+	routers       []router.Router
48
+	authZPlugins  []authorization.Plugin
49
+	routerSwapper *routerSwapper
49 50
 }
50 51
 
51 52
 // Addr contains string representation of address and its protocol (tcp, unix...).
... ...
@@ -80,12 +80,14 @@ func (s *Server) Close() {
80 80
 	}
81 81
 }
82 82
 
83
-// ServeAPI loops through all initialized servers and spawns goroutine
84
-// with Server method for each. It sets CreateMux() as Handler also.
85
-func (s *Server) ServeAPI() error {
83
+// serveAPI loops through all initialized servers and spawns goroutine
84
+// with Server method for each. It sets createMux() as Handler also.
85
+func (s *Server) serveAPI() error {
86
+	s.initRouterSwapper()
87
+
86 88
 	var chErrors = make(chan error, len(s.servers))
87 89
 	for _, srv := range s.servers {
88
-		srv.srv.Handler = s.CreateMux()
90
+		srv.srv.Handler = s.routerSwapper
89 91
 		go func(srv *HTTPServer) {
90 92
 			var err error
91 93
 			logrus.Infof("API listen on %s", srv.l.Addr())
... ...
@@ -186,11 +188,11 @@ func (s *Server) addRouter(r router.Router) {
186 186
 	s.routers = append(s.routers, r)
187 187
 }
188 188
 
189
-// CreateMux initializes the main router the server uses.
189
+// createMux initializes the main router the server uses.
190 190
 // we keep enableCors just for legacy usage, need to be removed in the future
191
-func (s *Server) CreateMux() *mux.Router {
191
+func (s *Server) createMux() *mux.Router {
192 192
 	m := mux.NewRouter()
193
-	if os.Getenv("DEBUG") != "" {
193
+	if utils.IsDebugEnabled() {
194 194
 		profilerSetup(m, "/debug/")
195 195
 	}
196 196
 
... ...
@@ -207,3 +209,36 @@ func (s *Server) CreateMux() *mux.Router {
207 207
 
208 208
 	return m
209 209
 }
210
+
211
+// Wait blocks the server goroutine until it exits.
212
+// It sends an error message if there is any error during
213
+// the API execution.
214
+func (s *Server) Wait(waitChan chan error) {
215
+	if err := s.serveAPI(); err != nil {
216
+		logrus.Errorf("ServeAPI error: %v", err)
217
+		waitChan <- err
218
+		return
219
+	}
220
+	waitChan <- nil
221
+}
222
+
223
+func (s *Server) initRouterSwapper() {
224
+	s.routerSwapper = &routerSwapper{
225
+		router: s.createMux(),
226
+	}
227
+}
228
+
229
+// Reload reads configuration changes and modifies the
230
+// server according to those changes.
231
+// Currently, only the --debug configuration is taken into account.
232
+func (s *Server) Reload(config *daemon.Config) {
233
+	debugEnabled := utils.IsDebugEnabled()
234
+	switch {
235
+	case debugEnabled && !config.Debug: // disable debug
236
+		utils.DisableDebug()
237
+		s.routerSwapper.Swap(s.createMux())
238
+	case config.Debug && !debugEnabled: // enable debug
239
+		utils.EnableDebug()
240
+		s.routerSwapper.Swap(s.createMux())
241
+	}
242
+}
... ...
@@ -1,9 +1,19 @@
1 1
 package daemon
2 2
 
3 3
 import (
4
+	"bytes"
5
+	"encoding/json"
6
+	"fmt"
7
+	"io"
8
+	"io/ioutil"
9
+	"strings"
10
+	"sync"
11
+
12
+	"github.com/Sirupsen/logrus"
4 13
 	"github.com/docker/docker/opts"
14
+	"github.com/docker/docker/pkg/discovery"
5 15
 	flag "github.com/docker/docker/pkg/mflag"
6
-	"github.com/docker/engine-api/types/container"
16
+	"github.com/imdario/mergo"
7 17
 )
8 18
 
9 19
 const (
... ...
@@ -11,42 +21,69 @@ const (
11 11
 	disableNetworkBridge = "none"
12 12
 )
13 13
 
14
+// LogConfig represents the default log configuration.
15
+// It includes json tags to deserialize configuration from a file
16
+// using the same names that the flags in the command line uses.
17
+type LogConfig struct {
18
+	Type   string            `json:"log-driver,omitempty"`
19
+	Config map[string]string `json:"log-opts,omitempty"`
20
+}
21
+
22
+// CommonTLSOptions defines TLS configuration for the daemon server.
23
+// It includes json tags to deserialize configuration from a file
24
+// using the same names that the flags in the command line uses.
25
+type CommonTLSOptions struct {
26
+	CAFile   string `json:"tlscacert,omitempty"`
27
+	CertFile string `json:"tlscert,omitempty"`
28
+	KeyFile  string `json:"tlskey,omitempty"`
29
+}
30
+
14 31
 // CommonConfig defines the configuration of a docker daemon which are
15 32
 // common across platforms.
33
+// It includes json tags to deserialize configuration from a file
34
+// using the same names that the flags in the command line uses.
16 35
 type CommonConfig struct {
17
-	AuthorizationPlugins []string // AuthorizationPlugins holds list of authorization plugins
18
-	AutoRestart          bool
19
-	Bridge               bridgeConfig // Bridge holds bridge network specific configuration.
20
-	Context              map[string][]string
21
-	DisableBridge        bool
22
-	DNS                  []string
23
-	DNSOptions           []string
24
-	DNSSearch            []string
25
-	ExecOptions          []string
26
-	ExecRoot             string
27
-	GraphDriver          string
28
-	GraphOptions         []string
29
-	Labels               []string
30
-	LogConfig            container.LogConfig
31
-	Mtu                  int
32
-	Pidfile              string
33
-	RemappedRoot         string
34
-	Root                 string
35
-	TrustKeyPath         string
36
+	AuthorizationPlugins []string            `json:"authorization-plugins,omitempty"` // AuthorizationPlugins holds list of authorization plugins
37
+	AutoRestart          bool                `json:"-"`
38
+	Bridge               bridgeConfig        `json:"-"` // Bridge holds bridge network specific configuration.
39
+	Context              map[string][]string `json:"-"`
40
+	DisableBridge        bool                `json:"-"`
41
+	DNS                  []string            `json:"dns,omitempty"`
42
+	DNSOptions           []string            `json:"dns-opts,omitempty"`
43
+	DNSSearch            []string            `json:"dns-search,omitempty"`
44
+	ExecOptions          []string            `json:"exec-opts,omitempty"`
45
+	ExecRoot             string              `json:"exec-root,omitempty"`
46
+	GraphDriver          string              `json:"storage-driver,omitempty"`
47
+	GraphOptions         []string            `json:"storage-opts,omitempty"`
48
+	Labels               []string            `json:"labels,omitempty"`
49
+	LogConfig            LogConfig           `json:"log-config,omitempty"`
50
+	Mtu                  int                 `json:"mtu,omitempty"`
51
+	Pidfile              string              `json:"pidfile,omitempty"`
52
+	Root                 string              `json:"graph,omitempty"`
53
+	TrustKeyPath         string              `json:"-"`
36 54
 
37 55
 	// ClusterStore is the storage backend used for the cluster information. It is used by both
38 56
 	// multihost networking (to store networks and endpoints information) and by the node discovery
39 57
 	// mechanism.
40
-	ClusterStore string
58
+	ClusterStore string `json:"cluster-store,omitempty"`
41 59
 
42 60
 	// ClusterOpts is used to pass options to the discovery package for tuning libkv settings, such
43 61
 	// as TLS configuration settings.
44
-	ClusterOpts map[string]string
62
+	ClusterOpts map[string]string `json:"cluster-store-opts,omitempty"`
45 63
 
46 64
 	// ClusterAdvertise is the network endpoint that the Engine advertises for the purpose of node
47 65
 	// discovery. This should be a 'host:port' combination on which that daemon instance is
48 66
 	// reachable by other hosts.
49
-	ClusterAdvertise string
67
+	ClusterAdvertise string `json:"cluster-advertise,omitempty"`
68
+
69
+	Debug      bool             `json:"debug,omitempty"`
70
+	Hosts      []string         `json:"hosts,omitempty"`
71
+	LogLevel   string           `json:"log-level,omitempty"`
72
+	TLS        bool             `json:"tls,omitempty"`
73
+	TLSVerify  bool             `json:"tls-verify,omitempty"`
74
+	TLSOptions CommonTLSOptions `json:"tls-opts,omitempty"`
75
+
76
+	reloadLock sync.Mutex
50 77
 }
51 78
 
52 79
 // InstallCommonFlags adds command-line options to the top-level flag parser for
... ...
@@ -54,9 +91,9 @@ type CommonConfig struct {
54 54
 // Subsequent calls to `flag.Parse` will populate config with values parsed
55 55
 // from the command-line.
56 56
 func (config *Config) InstallCommonFlags(cmd *flag.FlagSet, usageFn func(string) string) {
57
-	cmd.Var(opts.NewListOptsRef(&config.GraphOptions, nil), []string{"-storage-opt"}, usageFn("Set storage driver options"))
58
-	cmd.Var(opts.NewListOptsRef(&config.AuthorizationPlugins, nil), []string{"-authorization-plugin"}, usageFn("List authorization plugins in order from first evaluator to last"))
59
-	cmd.Var(opts.NewListOptsRef(&config.ExecOptions, nil), []string{"-exec-opt"}, usageFn("Set exec driver options"))
57
+	cmd.Var(opts.NewNamedListOptsRef("storage-opts", &config.GraphOptions, nil), []string{"-storage-opt"}, usageFn("Set storage driver options"))
58
+	cmd.Var(opts.NewNamedListOptsRef("authorization-plugins", &config.AuthorizationPlugins, nil), []string{"-authorization-plugin"}, usageFn("List authorization plugins in order from first evaluator to last"))
59
+	cmd.Var(opts.NewNamedListOptsRef("exec-opts", &config.ExecOptions, nil), []string{"-exec-opt"}, usageFn("Set exec driver options"))
60 60
 	cmd.StringVar(&config.Pidfile, []string{"p", "-pidfile"}, defaultPidFile, usageFn("Path to use for daemon PID file"))
61 61
 	cmd.StringVar(&config.Root, []string{"g", "-graph"}, defaultGraph, usageFn("Root of the Docker runtime"))
62 62
 	cmd.StringVar(&config.ExecRoot, []string{"-exec-root"}, "/var/run/docker", usageFn("Root of the Docker execdriver"))
... ...
@@ -65,12 +102,131 @@ func (config *Config) InstallCommonFlags(cmd *flag.FlagSet, usageFn func(string)
65 65
 	cmd.IntVar(&config.Mtu, []string{"#mtu", "-mtu"}, 0, usageFn("Set the containers network MTU"))
66 66
 	// FIXME: why the inconsistency between "hosts" and "sockets"?
67 67
 	cmd.Var(opts.NewListOptsRef(&config.DNS, opts.ValidateIPAddress), []string{"#dns", "-dns"}, usageFn("DNS server to use"))
68
-	cmd.Var(opts.NewListOptsRef(&config.DNSOptions, nil), []string{"-dns-opt"}, usageFn("DNS options to use"))
68
+	cmd.Var(opts.NewNamedListOptsRef("dns-opts", &config.DNSOptions, nil), []string{"-dns-opt"}, usageFn("DNS options to use"))
69 69
 	cmd.Var(opts.NewListOptsRef(&config.DNSSearch, opts.ValidateDNSSearch), []string{"-dns-search"}, usageFn("DNS search domains to use"))
70
-	cmd.Var(opts.NewListOptsRef(&config.Labels, opts.ValidateLabel), []string{"-label"}, usageFn("Set key=value labels to the daemon"))
70
+	cmd.Var(opts.NewNamedListOptsRef("labels", &config.Labels, opts.ValidateLabel), []string{"-label"}, usageFn("Set key=value labels to the daemon"))
71 71
 	cmd.StringVar(&config.LogConfig.Type, []string{"-log-driver"}, "json-file", usageFn("Default driver for container logs"))
72
-	cmd.Var(opts.NewMapOpts(config.LogConfig.Config, nil), []string{"-log-opt"}, usageFn("Set log driver options"))
72
+	cmd.Var(opts.NewNamedMapOpts("log-opts", config.LogConfig.Config, nil), []string{"-log-opt"}, usageFn("Set log driver options"))
73 73
 	cmd.StringVar(&config.ClusterAdvertise, []string{"-cluster-advertise"}, "", usageFn("Address or interface name to advertise"))
74 74
 	cmd.StringVar(&config.ClusterStore, []string{"-cluster-store"}, "", usageFn("Set the cluster store"))
75
-	cmd.Var(opts.NewMapOpts(config.ClusterOpts, nil), []string{"-cluster-store-opt"}, usageFn("Set cluster store options"))
75
+	cmd.Var(opts.NewNamedMapOpts("cluster-store-opts", config.ClusterOpts, nil), []string{"-cluster-store-opt"}, usageFn("Set cluster store options"))
76
+}
77
+
78
+func parseClusterAdvertiseSettings(clusterStore, clusterAdvertise string) (string, error) {
79
+	if clusterAdvertise == "" {
80
+		return "", errDiscoveryDisabled
81
+	}
82
+	if clusterStore == "" {
83
+		return "", fmt.Errorf("invalid cluster configuration. --cluster-advertise must be accompanied by --cluster-store configuration")
84
+	}
85
+
86
+	advertise, err := discovery.ParseAdvertise(clusterAdvertise)
87
+	if err != nil {
88
+		return "", fmt.Errorf("discovery advertise parsing failed (%v)", err)
89
+	}
90
+	return advertise, nil
91
+}
92
+
93
+// ReloadConfiguration reads the configuration in the host and reloads the daemon and server.
94
+func ReloadConfiguration(configFile string, flags *flag.FlagSet, reload func(*Config)) {
95
+	logrus.Infof("Got signal to reload configuration, reloading from: %s", configFile)
96
+	newConfig, err := getConflictFreeConfiguration(configFile, flags)
97
+	if err != nil {
98
+		logrus.Error(err)
99
+	} else {
100
+		reload(newConfig)
101
+	}
102
+}
103
+
104
+// MergeDaemonConfigurations reads a configuration file,
105
+// loads the file configuration in an isolated structure,
106
+// and merges the configuration provided from flags on top
107
+// if there are no conflicts.
108
+func MergeDaemonConfigurations(flagsConfig *Config, flags *flag.FlagSet, configFile string) (*Config, error) {
109
+	fileConfig, err := getConflictFreeConfiguration(configFile, flags)
110
+	if err != nil {
111
+		return nil, err
112
+	}
113
+
114
+	// merge flags configuration on top of the file configuration
115
+	if err := mergo.Merge(fileConfig, flagsConfig); err != nil {
116
+		return nil, err
117
+	}
118
+
119
+	return fileConfig, nil
120
+}
121
+
122
+// getConflictFreeConfiguration loads the configuration from a JSON file.
123
+// It compares that configuration with the one provided by the flags,
124
+// and returns an error if there are conflicts.
125
+func getConflictFreeConfiguration(configFile string, flags *flag.FlagSet) (*Config, error) {
126
+	b, err := ioutil.ReadFile(configFile)
127
+	if err != nil {
128
+		return nil, err
129
+	}
130
+
131
+	var reader io.Reader
132
+	if flags != nil {
133
+		var jsonConfig map[string]interface{}
134
+		reader = bytes.NewReader(b)
135
+		if err := json.NewDecoder(reader).Decode(&jsonConfig); err != nil {
136
+			return nil, err
137
+		}
138
+
139
+		if err := findConfigurationConflicts(jsonConfig, flags); err != nil {
140
+			return nil, err
141
+		}
142
+	}
143
+
144
+	var config Config
145
+	reader = bytes.NewReader(b)
146
+	err = json.NewDecoder(reader).Decode(&config)
147
+	return &config, err
148
+}
149
+
150
+// findConfigurationConflicts iterates over the provided flags searching for
151
+// duplicated configurations. It returns an error with all the conflicts if
152
+// it finds any.
153
+func findConfigurationConflicts(config map[string]interface{}, flags *flag.FlagSet) error {
154
+	var conflicts []string
155
+	flatten := make(map[string]interface{})
156
+	for k, v := range config {
157
+		if m, ok := v.(map[string]interface{}); ok {
158
+			for km, vm := range m {
159
+				flatten[km] = vm
160
+			}
161
+		} else {
162
+			flatten[k] = v
163
+		}
164
+	}
165
+
166
+	printConflict := func(name string, flagValue, fileValue interface{}) string {
167
+		return fmt.Sprintf("%s: (from flag: %v, from file: %v)", name, flagValue, fileValue)
168
+	}
169
+
170
+	collectConflicts := func(f *flag.Flag) {
171
+		// search option name in the json configuration payload if the value is a named option
172
+		if namedOption, ok := f.Value.(opts.NamedOption); ok {
173
+			if optsValue, ok := flatten[namedOption.Name()]; ok {
174
+				conflicts = append(conflicts, printConflict(namedOption.Name(), f.Value.String(), optsValue))
175
+			}
176
+		} else {
177
+			// search flag name in the json configuration payload without trailing dashes
178
+			for _, name := range f.Names {
179
+				name = strings.TrimLeft(name, "-")
180
+
181
+				if value, ok := flatten[name]; ok {
182
+					conflicts = append(conflicts, printConflict(name, f.Value.String(), value))
183
+					break
184
+				}
185
+			}
186
+		}
187
+	}
188
+
189
+	flags.Visit(collectConflicts)
190
+
191
+	if len(conflicts) > 0 {
192
+		return fmt.Errorf("the following directives are specified both as a flag and in the configuration file: %s", strings.Join(conflicts, ", "))
193
+	}
194
+	return nil
76 195
 }
77 196
new file mode 100644
... ...
@@ -0,0 +1,177 @@
0
+package daemon
1
+
2
+import (
3
+	"io/ioutil"
4
+	"os"
5
+	"strings"
6
+	"testing"
7
+
8
+	"github.com/docker/docker/opts"
9
+	"github.com/docker/docker/pkg/mflag"
10
+)
11
+
12
+func TestDaemonConfigurationMerge(t *testing.T) {
13
+	f, err := ioutil.TempFile("", "docker-config-")
14
+	if err != nil {
15
+		t.Fatal(err)
16
+	}
17
+
18
+	configFile := f.Name()
19
+	f.Write([]byte(`{"debug": true}`))
20
+	f.Close()
21
+
22
+	c := &Config{
23
+		CommonConfig: CommonConfig{
24
+			AutoRestart: true,
25
+			LogConfig: LogConfig{
26
+				Type:   "syslog",
27
+				Config: map[string]string{"tag": "test"},
28
+			},
29
+		},
30
+	}
31
+
32
+	cc, err := MergeDaemonConfigurations(c, nil, configFile)
33
+	if err != nil {
34
+		t.Fatal(err)
35
+	}
36
+	if !cc.Debug {
37
+		t.Fatalf("expected %v, got %v\n", true, cc.Debug)
38
+	}
39
+	if !cc.AutoRestart {
40
+		t.Fatalf("expected %v, got %v\n", true, cc.AutoRestart)
41
+	}
42
+	if cc.LogConfig.Type != "syslog" {
43
+		t.Fatalf("expected syslog config, got %q\n", cc.LogConfig)
44
+	}
45
+}
46
+
47
+func TestDaemonConfigurationNotFound(t *testing.T) {
48
+	_, err := MergeDaemonConfigurations(&Config{}, nil, "/tmp/foo-bar-baz-docker")
49
+	if err == nil || !os.IsNotExist(err) {
50
+		t.Fatalf("expected does not exist error, got %v", err)
51
+	}
52
+}
53
+
54
+func TestDaemonBrokenConfiguration(t *testing.T) {
55
+	f, err := ioutil.TempFile("", "docker-config-")
56
+	if err != nil {
57
+		t.Fatal(err)
58
+	}
59
+
60
+	configFile := f.Name()
61
+	f.Write([]byte(`{"Debug": tru`))
62
+	f.Close()
63
+
64
+	_, err = MergeDaemonConfigurations(&Config{}, nil, configFile)
65
+	if err == nil {
66
+		t.Fatalf("expected error, got %v", err)
67
+	}
68
+}
69
+
70
+func TestParseClusterAdvertiseSettings(t *testing.T) {
71
+	_, err := parseClusterAdvertiseSettings("something", "")
72
+	if err != errDiscoveryDisabled {
73
+		t.Fatalf("expected discovery disabled error, got %v\n", err)
74
+	}
75
+
76
+	_, err = parseClusterAdvertiseSettings("", "something")
77
+	if err == nil {
78
+		t.Fatalf("expected discovery store error, got %v\n", err)
79
+	}
80
+
81
+	_, err = parseClusterAdvertiseSettings("etcd", "127.0.0.1:8080")
82
+	if err != nil {
83
+		t.Fatal(err)
84
+	}
85
+}
86
+
87
+func TestFindConfigurationConflicts(t *testing.T) {
88
+	config := map[string]interface{}{"authorization-plugins": "foobar"}
89
+	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
90
+
91
+	err := findConfigurationConflicts(config, flags)
92
+	if err != nil {
93
+		t.Fatal(err)
94
+	}
95
+
96
+	flags.String([]string{"authorization-plugins"}, "", "")
97
+	if err := flags.Set("authorization-plugins", "asdf"); err != nil {
98
+		t.Fatal(err)
99
+	}
100
+
101
+	err = findConfigurationConflicts(config, flags)
102
+	if err == nil {
103
+		t.Fatal("expected error, got nil")
104
+	}
105
+	if !strings.Contains(err.Error(), "authorization-plugins") {
106
+		t.Fatalf("expected authorization-plugins conflict, got %v", err)
107
+	}
108
+}
109
+
110
+func TestFindConfigurationConflictsWithNamedOptions(t *testing.T) {
111
+	config := map[string]interface{}{"hosts": []string{"qwer"}}
112
+	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
113
+
114
+	var hosts []string
115
+	flags.Var(opts.NewNamedListOptsRef("hosts", &hosts, opts.ValidateHost), []string{"H", "-host"}, "Daemon socket(s) to connect to")
116
+	if err := flags.Set("-host", "tcp://127.0.0.1:4444"); err != nil {
117
+		t.Fatal(err)
118
+	}
119
+	if err := flags.Set("H", "unix:///var/run/docker.sock"); err != nil {
120
+		t.Fatal(err)
121
+	}
122
+
123
+	err := findConfigurationConflicts(config, flags)
124
+	if err == nil {
125
+		t.Fatal("expected error, got nil")
126
+	}
127
+	if !strings.Contains(err.Error(), "hosts") {
128
+		t.Fatalf("expected hosts conflict, got %v", err)
129
+	}
130
+}
131
+
132
+func TestDaemonConfigurationMergeConflicts(t *testing.T) {
133
+	f, err := ioutil.TempFile("", "docker-config-")
134
+	if err != nil {
135
+		t.Fatal(err)
136
+	}
137
+
138
+	configFile := f.Name()
139
+	f.Write([]byte(`{"debug": true}`))
140
+	f.Close()
141
+
142
+	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
143
+	flags.Bool([]string{"debug"}, false, "")
144
+	flags.Set("debug", "false")
145
+
146
+	_, err = MergeDaemonConfigurations(&Config{}, flags, configFile)
147
+	if err == nil {
148
+		t.Fatal("expected error, got nil")
149
+	}
150
+	if !strings.Contains(err.Error(), "debug") {
151
+		t.Fatalf("expected debug conflict, got %v", err)
152
+	}
153
+}
154
+
155
+func TestDaemonConfigurationMergeConflictsWithInnerStructs(t *testing.T) {
156
+	f, err := ioutil.TempFile("", "docker-config-")
157
+	if err != nil {
158
+		t.Fatal(err)
159
+	}
160
+
161
+	configFile := f.Name()
162
+	f.Write([]byte(`{"tlscacert": "/etc/certificates/ca.pem"}`))
163
+	f.Close()
164
+
165
+	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
166
+	flags.String([]string{"tlscacert"}, "", "")
167
+	flags.Set("tlscacert", "~/.docker/ca.pem")
168
+
169
+	_, err = MergeDaemonConfigurations(&Config{}, flags, configFile)
170
+	if err == nil {
171
+		t.Fatal("expected error, got nil")
172
+	}
173
+	if !strings.Contains(err.Error(), "tlscacert") {
174
+		t.Fatalf("expected tlscacert conflict, got %v", err)
175
+	}
176
+}
... ...
@@ -18,18 +18,20 @@ var (
18 18
 )
19 19
 
20 20
 // Config defines the configuration of a docker daemon.
21
+// It includes json tags to deserialize configuration from a file
22
+// using the same names that the flags in the command line uses.
21 23
 type Config struct {
22 24
 	CommonConfig
23 25
 
24 26
 	// Fields below here are platform specific.
25 27
 
26
-	CorsHeaders          string
27
-	EnableCors           bool
28
-	EnableSelinuxSupport bool
29
-	RemappedRoot         string
30
-	SocketGroup          string
31
-	CgroupParent         string
32
-	Ulimits              map[string]*units.Ulimit
28
+	CorsHeaders          string                   `json:"api-cors-headers,omitempty"`
29
+	EnableCors           bool                     `json:"api-enable-cors,omitempty"`
30
+	EnableSelinuxSupport bool                     `json:"selinux-enabled,omitempty"`
31
+	RemappedRoot         string                   `json:"userns-remap,omitempty"`
32
+	SocketGroup          string                   `json:"group,omitempty"`
33
+	CgroupParent         string                   `json:"cgroup-parent,omitempty"`
34
+	Ulimits              map[string]*units.Ulimit `json:"default-ulimits,omitempty"`
33 35
 }
34 36
 
35 37
 // bridgeConfig stores all the bridge driver specific
... ...
@@ -46,7 +46,6 @@ import (
46 46
 	"github.com/docker/docker/layer"
47 47
 	"github.com/docker/docker/migrate/v1"
48 48
 	"github.com/docker/docker/pkg/archive"
49
-	"github.com/docker/docker/pkg/discovery"
50 49
 	"github.com/docker/docker/pkg/fileutils"
51 50
 	"github.com/docker/docker/pkg/graphdb"
52 51
 	"github.com/docker/docker/pkg/idtools"
... ...
@@ -155,7 +154,7 @@ type Daemon struct {
155 155
 	EventsService             *events.Events
156 156
 	netController             libnetwork.NetworkController
157 157
 	volumes                   *store.VolumeStore
158
-	discoveryWatcher          discovery.Watcher
158
+	discoveryWatcher          discoveryReloader
159 159
 	root                      string
160 160
 	seccompEnabled            bool
161 161
 	shutdown                  bool
... ...
@@ -292,7 +291,7 @@ func (daemon *Daemon) Register(container *container.Container) error {
292 292
 
293 293
 func (daemon *Daemon) restore() error {
294 294
 	var (
295
-		debug         = os.Getenv("DEBUG") != ""
295
+		debug         = utils.IsDebugEnabled()
296 296
 		currentDriver = daemon.GraphDriverName()
297 297
 		containers    = make(map[string]*container.Container)
298 298
 	)
... ...
@@ -772,19 +771,8 @@ func NewDaemon(config *Config, registryService *registry.Service) (daemon *Daemo
772 772
 
773 773
 	// Discovery is only enabled when the daemon is launched with an address to advertise.  When
774 774
 	// initialized, the daemon is registered and we can store the discovery backend as its read-only
775
-	// DiscoveryWatcher version.
776
-	if config.ClusterStore != "" && config.ClusterAdvertise != "" {
777
-		advertise, err := discovery.ParseAdvertise(config.ClusterStore, config.ClusterAdvertise)
778
-		if err != nil {
779
-			return nil, fmt.Errorf("discovery advertise parsing failed (%v)", err)
780
-		}
781
-		config.ClusterAdvertise = advertise
782
-		d.discoveryWatcher, err = initDiscovery(config.ClusterStore, config.ClusterAdvertise, config.ClusterOpts)
783
-		if err != nil {
784
-			return nil, fmt.Errorf("discovery initialization failed (%v)", err)
785
-		}
786
-	} else if config.ClusterAdvertise != "" {
787
-		return nil, fmt.Errorf("invalid cluster configuration. --cluster-advertise must be accompanied by --cluster-store configuration")
775
+	if err := d.initDiscovery(config); err != nil {
776
+		return nil, err
788 777
 	}
789 778
 
790 779
 	d.netController, err = d.initNetworkController(config)
... ...
@@ -815,7 +803,10 @@ func NewDaemon(config *Config, registryService *registry.Service) (daemon *Daemo
815 815
 	d.configStore = config
816 816
 	d.execDriver = ed
817 817
 	d.statsCollector = d.newStatsCollector(1 * time.Second)
818
-	d.defaultLogConfig = config.LogConfig
818
+	d.defaultLogConfig = containertypes.LogConfig{
819
+		Type:   config.LogConfig.Type,
820
+		Config: config.LogConfig.Config,
821
+	}
819 822
 	d.RegistryService = registryService
820 823
 	d.EventsService = eventsService
821 824
 	d.volumes = volStore
... ...
@@ -1521,6 +1512,76 @@ func (daemon *Daemon) newBaseContainer(id string) *container.Container {
1521 1521
 	return container.NewBaseContainer(id, daemon.containerRoot(id))
1522 1522
 }
1523 1523
 
1524
+// initDiscovery initializes the discovery watcher for this daemon.
1525
+func (daemon *Daemon) initDiscovery(config *Config) error {
1526
+	advertise, err := parseClusterAdvertiseSettings(config.ClusterStore, config.ClusterAdvertise)
1527
+	if err != nil {
1528
+		if err == errDiscoveryDisabled {
1529
+			return nil
1530
+		}
1531
+		return err
1532
+	}
1533
+
1534
+	config.ClusterAdvertise = advertise
1535
+	discoveryWatcher, err := initDiscovery(config.ClusterStore, config.ClusterAdvertise, config.ClusterOpts)
1536
+	if err != nil {
1537
+		return fmt.Errorf("discovery initialization failed (%v)", err)
1538
+	}
1539
+
1540
+	daemon.discoveryWatcher = discoveryWatcher
1541
+	return nil
1542
+}
1543
+
1544
+// Reload reads configuration changes and modifies the
1545
+// daemon according to those changes.
1546
+// This are the settings that Reload changes:
1547
+// - Daemon labels.
1548
+// - Cluster discovery (reconfigure and restart).
1549
+func (daemon *Daemon) Reload(config *Config) error {
1550
+	daemon.configStore.reloadLock.Lock()
1551
+	defer daemon.configStore.reloadLock.Unlock()
1552
+
1553
+	daemon.configStore.Labels = config.Labels
1554
+	return daemon.reloadClusterDiscovery(config)
1555
+}
1556
+
1557
+func (daemon *Daemon) reloadClusterDiscovery(config *Config) error {
1558
+	newAdvertise, err := parseClusterAdvertiseSettings(config.ClusterStore, config.ClusterAdvertise)
1559
+	if err != nil && err != errDiscoveryDisabled {
1560
+		return err
1561
+	}
1562
+
1563
+	// check discovery modifications
1564
+	if !modifiedDiscoverySettings(daemon.configStore, newAdvertise, config.ClusterStore, config.ClusterOpts) {
1565
+		return nil
1566
+	}
1567
+
1568
+	// enable discovery for the first time if it was not previously enabled
1569
+	if daemon.discoveryWatcher == nil {
1570
+		discoveryWatcher, err := initDiscovery(config.ClusterStore, newAdvertise, config.ClusterOpts)
1571
+		if err != nil {
1572
+			return fmt.Errorf("discovery initialization failed (%v)", err)
1573
+		}
1574
+		daemon.discoveryWatcher = discoveryWatcher
1575
+	} else {
1576
+		if err == errDiscoveryDisabled {
1577
+			// disable discovery if it was previously enabled and it's disabled now
1578
+			daemon.discoveryWatcher.Stop()
1579
+		} else {
1580
+			// reload discovery
1581
+			if err = daemon.discoveryWatcher.Reload(config.ClusterStore, newAdvertise, config.ClusterOpts); err != nil {
1582
+				return err
1583
+			}
1584
+		}
1585
+	}
1586
+
1587
+	daemon.configStore.ClusterStore = config.ClusterStore
1588
+	daemon.configStore.ClusterOpts = config.ClusterOpts
1589
+	daemon.configStore.ClusterAdvertise = newAdvertise
1590
+
1591
+	return nil
1592
+}
1593
+
1524 1594
 func convertLnNetworkStats(name string, stats *lntypes.InterfaceStatistics) *libcontainer.NetworkInterface {
1525 1595
 	n := &libcontainer.NetworkInterface{Name: name}
1526 1596
 	n.RxBytes = stats.RxBytes
... ...
@@ -4,9 +4,13 @@ import (
4 4
 	"io/ioutil"
5 5
 	"os"
6 6
 	"path/filepath"
7
+	"reflect"
7 8
 	"testing"
9
+	"time"
8 10
 
9 11
 	"github.com/docker/docker/container"
12
+	"github.com/docker/docker/pkg/discovery"
13
+	_ "github.com/docker/docker/pkg/discovery/memory"
10 14
 	"github.com/docker/docker/pkg/registrar"
11 15
 	"github.com/docker/docker/pkg/truncindex"
12 16
 	"github.com/docker/docker/volume"
... ...
@@ -371,3 +375,118 @@ func TestMerge(t *testing.T) {
371 371
 		}
372 372
 	}
373 373
 }
374
+
375
+func TestDaemonReloadLabels(t *testing.T) {
376
+	daemon := &Daemon{}
377
+	daemon.configStore = &Config{
378
+		CommonConfig: CommonConfig{
379
+			Labels: []string{"foo:bar"},
380
+		},
381
+	}
382
+
383
+	newConfig := &Config{
384
+		CommonConfig: CommonConfig{
385
+			Labels: []string{"foo:baz"},
386
+		},
387
+	}
388
+
389
+	daemon.Reload(newConfig)
390
+	label := daemon.configStore.Labels[0]
391
+	if label != "foo:baz" {
392
+		t.Fatalf("Expected daemon label `foo:baz`, got %s", label)
393
+	}
394
+}
395
+
396
+func TestDaemonDiscoveryReload(t *testing.T) {
397
+	daemon := &Daemon{}
398
+	daemon.configStore = &Config{
399
+		CommonConfig: CommonConfig{
400
+			ClusterStore:     "memory://127.0.0.1",
401
+			ClusterAdvertise: "127.0.0.1:3333",
402
+		},
403
+	}
404
+
405
+	if err := daemon.initDiscovery(daemon.configStore); err != nil {
406
+		t.Fatal(err)
407
+	}
408
+
409
+	expected := discovery.Entries{
410
+		&discovery.Entry{Host: "127.0.0.1", Port: "3333"},
411
+	}
412
+
413
+	stopCh := make(chan struct{})
414
+	defer close(stopCh)
415
+	ch, errCh := daemon.discoveryWatcher.Watch(stopCh)
416
+
417
+	select {
418
+	case <-time.After(1 * time.Second):
419
+		t.Fatal("failed to get discovery advertisements in time")
420
+	case e := <-ch:
421
+		if !reflect.DeepEqual(e, expected) {
422
+			t.Fatalf("expected %v, got %v\n", expected, e)
423
+		}
424
+	case e := <-errCh:
425
+		t.Fatal(e)
426
+	}
427
+
428
+	newConfig := &Config{
429
+		CommonConfig: CommonConfig{
430
+			ClusterStore:     "memory://127.0.0.1:2222",
431
+			ClusterAdvertise: "127.0.0.1:5555",
432
+		},
433
+	}
434
+
435
+	expected = discovery.Entries{
436
+		&discovery.Entry{Host: "127.0.0.1", Port: "5555"},
437
+	}
438
+
439
+	if err := daemon.Reload(newConfig); err != nil {
440
+		t.Fatal(err)
441
+	}
442
+	ch, errCh = daemon.discoveryWatcher.Watch(stopCh)
443
+
444
+	select {
445
+	case <-time.After(1 * time.Second):
446
+		t.Fatal("failed to get discovery advertisements in time")
447
+	case e := <-ch:
448
+		if !reflect.DeepEqual(e, expected) {
449
+			t.Fatalf("expected %v, got %v\n", expected, e)
450
+		}
451
+	case e := <-errCh:
452
+		t.Fatal(e)
453
+	}
454
+}
455
+
456
+func TestDaemonDiscoveryReloadFromEmptyDiscovery(t *testing.T) {
457
+	daemon := &Daemon{}
458
+	daemon.configStore = &Config{}
459
+
460
+	newConfig := &Config{
461
+		CommonConfig: CommonConfig{
462
+			ClusterStore:     "memory://127.0.0.1:2222",
463
+			ClusterAdvertise: "127.0.0.1:5555",
464
+		},
465
+	}
466
+
467
+	expected := discovery.Entries{
468
+		&discovery.Entry{Host: "127.0.0.1", Port: "5555"},
469
+	}
470
+
471
+	if err := daemon.Reload(newConfig); err != nil {
472
+		t.Fatal(err)
473
+	}
474
+	stopCh := make(chan struct{})
475
+	defer close(stopCh)
476
+	ch, errCh := daemon.discoveryWatcher.Watch(stopCh)
477
+
478
+	select {
479
+	case <-time.After(1 * time.Second):
480
+		t.Fatal("failed to get discovery advertisements in time")
481
+	case e := <-ch:
482
+		if !reflect.DeepEqual(e, expected) {
483
+			t.Fatalf("expected %v, got %v\n", expected, e)
484
+		}
485
+	case e := <-errCh:
486
+		t.Fatal(e)
487
+	}
488
+}
... ...
@@ -1,7 +1,9 @@
1 1
 package daemon
2 2
 
3 3
 import (
4
+	"errors"
4 5
 	"fmt"
6
+	"reflect"
5 7
 	"strconv"
6 8
 	"time"
7 9
 
... ...
@@ -19,6 +21,24 @@ const (
19 19
 	defaultDiscoveryTTLFactor = 3
20 20
 )
21 21
 
22
+var errDiscoveryDisabled = errors.New("discovery is disabled")
23
+
24
+type discoveryReloader interface {
25
+	discovery.Watcher
26
+	Stop()
27
+	Reload(backend, address string, clusterOpts map[string]string) error
28
+}
29
+
30
+type daemonDiscoveryReloader struct {
31
+	backend discovery.Backend
32
+	ticker  *time.Ticker
33
+	term    chan bool
34
+}
35
+
36
+func (d *daemonDiscoveryReloader) Watch(stopCh <-chan struct{}) (<-chan discovery.Entries, <-chan error) {
37
+	return d.backend.Watch(stopCh)
38
+}
39
+
22 40
 func discoveryOpts(clusterOpts map[string]string) (time.Duration, time.Duration, error) {
23 41
 	var (
24 42
 		heartbeat = defaultDiscoveryHeartbeat
... ...
@@ -57,36 +77,94 @@ func discoveryOpts(clusterOpts map[string]string) (time.Duration, time.Duration,
57 57
 
58 58
 // initDiscovery initialized the nodes discovery subsystem by connecting to the specified backend
59 59
 // and start a registration loop to advertise the current node under the specified address.
60
-func initDiscovery(backend, address string, clusterOpts map[string]string) (discovery.Backend, error) {
61
-
62
-	heartbeat, ttl, err := discoveryOpts(clusterOpts)
60
+func initDiscovery(backendAddress, advertiseAddress string, clusterOpts map[string]string) (discoveryReloader, error) {
61
+	heartbeat, backend, err := parseDiscoveryOptions(backendAddress, clusterOpts)
63 62
 	if err != nil {
64 63
 		return nil, err
65 64
 	}
66 65
 
67
-	discoveryBackend, err := discovery.New(backend, heartbeat, ttl, clusterOpts)
68
-	if err != nil {
69
-		return nil, err
66
+	reloader := &daemonDiscoveryReloader{
67
+		backend: backend,
68
+		ticker:  time.NewTicker(heartbeat),
69
+		term:    make(chan bool),
70 70
 	}
71
-
72 71
 	// We call Register() on the discovery backend in a loop for the whole lifetime of the daemon,
73 72
 	// but we never actually Watch() for nodes appearing and disappearing for the moment.
74
-	go registrationLoop(discoveryBackend, address, heartbeat)
75
-	return discoveryBackend, nil
73
+	reloader.advertise(advertiseAddress)
74
+	return reloader, nil
76 75
 }
77 76
 
78
-func registerAddr(backend discovery.Backend, addr string) {
79
-	if err := backend.Register(addr); err != nil {
77
+func (d *daemonDiscoveryReloader) advertise(address string) {
78
+	d.registerAddr(address)
79
+	go d.advertiseHeartbeat(address)
80
+}
81
+
82
+func (d *daemonDiscoveryReloader) registerAddr(addr string) {
83
+	if err := d.backend.Register(addr); err != nil {
80 84
 		log.Warnf("Registering as %q in discovery failed: %v", addr, err)
81 85
 	}
82 86
 }
83 87
 
84
-// registrationLoop registers the current node against the discovery backend using the specified
88
+// advertiseHeartbeat registers the current node against the discovery backend using the specified
85 89
 // address. The function never returns, as registration against the backend comes with a TTL and
86 90
 // requires regular heartbeats.
87
-func registrationLoop(discoveryBackend discovery.Backend, address string, heartbeat time.Duration) {
88
-	registerAddr(discoveryBackend, address)
89
-	for range time.Tick(heartbeat) {
90
-		registerAddr(discoveryBackend, address)
91
+func (d *daemonDiscoveryReloader) advertiseHeartbeat(address string) {
92
+	for {
93
+		select {
94
+		case <-d.ticker.C:
95
+			d.registerAddr(address)
96
+		case <-d.term:
97
+			return
98
+		}
99
+	}
100
+}
101
+
102
+// Reload makes the watcher to stop advertising and reconfigures it to advertise in a new address.
103
+func (d *daemonDiscoveryReloader) Reload(backendAddress, advertiseAddress string, clusterOpts map[string]string) error {
104
+	d.Stop()
105
+
106
+	heartbeat, backend, err := parseDiscoveryOptions(backendAddress, clusterOpts)
107
+	if err != nil {
108
+		return err
109
+	}
110
+
111
+	d.backend = backend
112
+	d.ticker = time.NewTicker(heartbeat)
113
+
114
+	d.advertise(advertiseAddress)
115
+	return nil
116
+}
117
+
118
+// Stop terminates the discovery advertising.
119
+func (d *daemonDiscoveryReloader) Stop() {
120
+	d.ticker.Stop()
121
+	d.term <- true
122
+}
123
+
124
+func parseDiscoveryOptions(backendAddress string, clusterOpts map[string]string) (time.Duration, discovery.Backend, error) {
125
+	heartbeat, ttl, err := discoveryOpts(clusterOpts)
126
+	if err != nil {
127
+		return 0, nil, err
91 128
 	}
129
+
130
+	backend, err := discovery.New(backendAddress, heartbeat, ttl, clusterOpts)
131
+	if err != nil {
132
+		return 0, nil, err
133
+	}
134
+	return heartbeat, backend, nil
135
+}
136
+
137
+// modifiedDiscoverySettings returns whether the discovery configuration has been modified or not.
138
+func modifiedDiscoverySettings(config *Config, backendType, advertise string, clusterOpts map[string]string) bool {
139
+	if config.ClusterStore != backendType || config.ClusterAdvertise != advertise {
140
+		return true
141
+	}
142
+
143
+	if (config.ClusterOpts == nil && clusterOpts == nil) ||
144
+		(config.ClusterOpts == nil && len(clusterOpts) == 0) ||
145
+		(len(config.ClusterOpts) == 0 && clusterOpts == nil) {
146
+		return false
147
+	}
148
+
149
+	return !reflect.DeepEqual(config.ClusterOpts, clusterOpts)
92 150
 }
... ...
@@ -89,3 +89,64 @@ func TestDiscoveryOpts(t *testing.T) {
89 89
 		t.Fatalf("TTL - Expected : %v, Actual : %v", expected, ttl)
90 90
 	}
91 91
 }
92
+
93
+func TestModifiedDiscoverySettings(t *testing.T) {
94
+	cases := []struct {
95
+		current  *Config
96
+		modified *Config
97
+		expected bool
98
+	}{
99
+		{
100
+			current:  discoveryConfig("foo", "bar", map[string]string{}),
101
+			modified: discoveryConfig("foo", "bar", map[string]string{}),
102
+			expected: false,
103
+		},
104
+		{
105
+			current:  discoveryConfig("foo", "bar", map[string]string{"foo": "bar"}),
106
+			modified: discoveryConfig("foo", "bar", map[string]string{"foo": "bar"}),
107
+			expected: false,
108
+		},
109
+		{
110
+			current:  discoveryConfig("foo", "bar", map[string]string{}),
111
+			modified: discoveryConfig("foo", "bar", nil),
112
+			expected: false,
113
+		},
114
+		{
115
+			current:  discoveryConfig("foo", "bar", nil),
116
+			modified: discoveryConfig("foo", "bar", map[string]string{}),
117
+			expected: false,
118
+		},
119
+		{
120
+			current:  discoveryConfig("foo", "bar", nil),
121
+			modified: discoveryConfig("baz", "bar", nil),
122
+			expected: true,
123
+		},
124
+		{
125
+			current:  discoveryConfig("foo", "bar", nil),
126
+			modified: discoveryConfig("foo", "baz", nil),
127
+			expected: true,
128
+		},
129
+		{
130
+			current:  discoveryConfig("foo", "bar", nil),
131
+			modified: discoveryConfig("foo", "bar", map[string]string{"foo": "bar"}),
132
+			expected: true,
133
+		},
134
+	}
135
+
136
+	for _, c := range cases {
137
+		got := modifiedDiscoverySettings(c.current, c.modified.ClusterStore, c.modified.ClusterAdvertise, c.modified.ClusterOpts)
138
+		if c.expected != got {
139
+			t.Fatalf("expected %v, got %v: current config %q, new config %q", c.expected, got, c.current, c.modified)
140
+		}
141
+	}
142
+}
143
+
144
+func discoveryConfig(backendAddr, advertiseAddr string, opts map[string]string) *Config {
145
+	return &Config{
146
+		CommonConfig: CommonConfig{
147
+			ClusterStore:     backendAddr,
148
+			ClusterAdvertise: advertiseAddr,
149
+			ClusterOpts:      opts,
150
+		},
151
+	}
152
+}
... ...
@@ -79,7 +79,7 @@ func (daemon *Daemon) SystemInfo() (*types.Info, error) {
79 79
 		IPv4Forwarding:     !sysInfo.IPv4ForwardingDisabled,
80 80
 		BridgeNfIptables:   !sysInfo.BridgeNfCallIptablesDisabled,
81 81
 		BridgeNfIP6tables:  !sysInfo.BridgeNfCallIP6tablesDisabled,
82
-		Debug:              os.Getenv("DEBUG") != "",
82
+		Debug:              utils.IsDebugEnabled(),
83 83
 		NFd:                fileutils.GetTotalUsedFds(),
84 84
 		NGoroutines:        runtime.NumGoroutine(),
85 85
 		SystemTime:         time.Now().Format(time.RFC3339Nano),
... ...
@@ -21,7 +21,6 @@ const (
21 21
 )
22 22
 
23 23
 var (
24
-	daemonFlags *flag.FlagSet
25 24
 	commonFlags = &cli.CommonFlags{FlagSet: new(flag.FlagSet)}
26 25
 
27 26
 	dockerCertPath  = os.Getenv("DOCKER_CERT_PATH")
... ...
@@ -50,7 +49,7 @@ func init() {
50 50
 	cmd.StringVar(&tlsOptions.CertFile, []string{"-tlscert"}, filepath.Join(dockerCertPath, defaultCertFile), "Path to TLS certificate file")
51 51
 	cmd.StringVar(&tlsOptions.KeyFile, []string{"-tlskey"}, filepath.Join(dockerCertPath, defaultKeyFile), "Path to TLS key file")
52 52
 
53
-	cmd.Var(opts.NewListOptsRef(&commonFlags.Hosts, opts.ValidateHost), []string{"H", "-host"}, "Daemon socket(s) to connect to")
53
+	cmd.Var(opts.NewNamedListOptsRef("hosts", &commonFlags.Hosts, opts.ValidateHost), []string{"H", "-host"}, "Daemon socket(s) to connect to")
54 54
 }
55 55
 
56 56
 func postParseCommon() {
... ...
@@ -67,11 +66,6 @@ func postParseCommon() {
67 67
 		logrus.SetLevel(logrus.InfoLevel)
68 68
 	}
69 69
 
70
-	if commonFlags.Debug {
71
-		os.Setenv("DEBUG", "1")
72
-		logrus.SetLevel(logrus.DebugLevel)
73
-	}
74
-
75 70
 	// Regardless of whether the user sets it to true or false, if they
76 71
 	// specify --tlsverify at all then we need to turn on tls
77 72
 	// TLSVerify can be true even if not set due to DOCKER_TLS_VERIFY env var, so we need to check that here as well
... ...
@@ -30,23 +30,34 @@ import (
30 30
 	"github.com/docker/go-connections/tlsconfig"
31 31
 )
32 32
 
33
-const daemonUsage = "       docker daemon [ --help | ... ]\n"
33
+const (
34
+	daemonUsage          = "       docker daemon [ --help | ... ]\n"
35
+	daemonConfigFileFlag = "-config-file"
36
+)
34 37
 
35 38
 var (
36 39
 	daemonCli cli.Handler = NewDaemonCli()
37 40
 )
38 41
 
42
+// DaemonCli represents the daemon CLI.
43
+type DaemonCli struct {
44
+	*daemon.Config
45
+	registryOptions *registry.Options
46
+	flags           *flag.FlagSet
47
+}
48
+
39 49
 func presentInHelp(usage string) string { return usage }
40 50
 func absentFromHelp(string) string      { return "" }
41 51
 
42 52
 // NewDaemonCli returns a pre-configured daemon CLI
43 53
 func NewDaemonCli() *DaemonCli {
44
-	daemonFlags = cli.Subcmd("daemon", nil, "Enable daemon mode", true)
54
+	daemonFlags := cli.Subcmd("daemon", nil, "Enable daemon mode", true)
45 55
 
46 56
 	// TODO(tiborvass): remove InstallFlags?
47 57
 	daemonConfig := new(daemon.Config)
48 58
 	daemonConfig.LogConfig.Config = make(map[string]string)
49 59
 	daemonConfig.ClusterOpts = make(map[string]string)
60
+
50 61
 	daemonConfig.InstallFlags(daemonFlags, presentInHelp)
51 62
 	daemonConfig.InstallFlags(flag.CommandLine, absentFromHelp)
52 63
 	registryOptions := new(registry.Options)
... ...
@@ -57,6 +68,7 @@ func NewDaemonCli() *DaemonCli {
57 57
 	return &DaemonCli{
58 58
 		Config:          daemonConfig,
59 59
 		registryOptions: registryOptions,
60
+		flags:           daemonFlags,
60 61
 	}
61 62
 }
62 63
 
... ...
@@ -101,12 +113,6 @@ func migrateKey() (err error) {
101 101
 	return nil
102 102
 }
103 103
 
104
-// DaemonCli represents the daemon CLI.
105
-type DaemonCli struct {
106
-	*daemon.Config
107
-	registryOptions *registry.Options
108
-}
109
-
110 104
 func getGlobalFlag() (globalFlag *flag.Flag) {
111 105
 	defer func() {
112 106
 		if x := recover(); x != nil {
... ...
@@ -136,15 +142,27 @@ func (cli *DaemonCli) CmdDaemon(args ...string) error {
136 136
 		os.Exit(1)
137 137
 	} else {
138 138
 		// allow new form `docker daemon -D`
139
-		flag.Merge(daemonFlags, commonFlags.FlagSet)
139
+		flag.Merge(cli.flags, commonFlags.FlagSet)
140 140
 	}
141 141
 
142
-	daemonFlags.ParseFlags(args, true)
142
+	configFile := cli.flags.String([]string{daemonConfigFileFlag}, defaultDaemonConfigFile, "Daemon configuration file")
143
+
144
+	cli.flags.ParseFlags(args, true)
143 145
 	commonFlags.PostParse()
144 146
 
145 147
 	if commonFlags.TrustKey == "" {
146 148
 		commonFlags.TrustKey = filepath.Join(getDaemonConfDir(), defaultTrustKeyFile)
147 149
 	}
150
+	cliConfig, err := loadDaemonCliConfig(cli.Config, cli.flags, commonFlags, *configFile)
151
+	if err != nil {
152
+		fmt.Fprint(os.Stderr, err)
153
+		os.Exit(1)
154
+	}
155
+	cli.Config = cliConfig
156
+
157
+	if cli.Config.Debug {
158
+		utils.EnableDebug()
159
+	}
148 160
 
149 161
 	if utils.ExperimentalBuild() {
150 162
 		logrus.Warn("Running experimental build")
... ...
@@ -184,12 +202,18 @@ func (cli *DaemonCli) CmdDaemon(args ...string) error {
184 184
 	serverConfig = setPlatformServerConfig(serverConfig, cli.Config)
185 185
 
186 186
 	defaultHost := opts.DefaultHost
187
-	if commonFlags.TLSOptions != nil {
188
-		if !commonFlags.TLSOptions.InsecureSkipVerify {
187
+	if cli.Config.TLS {
188
+		tlsOptions := tlsconfig.Options{
189
+			CAFile:   cli.Config.TLSOptions.CAFile,
190
+			CertFile: cli.Config.TLSOptions.CertFile,
191
+			KeyFile:  cli.Config.TLSOptions.KeyFile,
192
+		}
193
+
194
+		if cli.Config.TLSVerify {
189 195
 			// server requires and verifies client's certificate
190
-			commonFlags.TLSOptions.ClientAuth = tls.RequireAndVerifyClientCert
196
+			tlsOptions.ClientAuth = tls.RequireAndVerifyClientCert
191 197
 		}
192
-		tlsConfig, err := tlsconfig.Server(*commonFlags.TLSOptions)
198
+		tlsConfig, err := tlsconfig.Server(tlsOptions)
193 199
 		if err != nil {
194 200
 			logrus.Fatal(err)
195 201
 		}
... ...
@@ -197,22 +221,23 @@ func (cli *DaemonCli) CmdDaemon(args ...string) error {
197 197
 		defaultHost = opts.DefaultTLSHost
198 198
 	}
199 199
 
200
-	if len(commonFlags.Hosts) == 0 {
201
-		commonFlags.Hosts = make([]string, 1)
200
+	if len(cli.Config.Hosts) == 0 {
201
+		cli.Config.Hosts = make([]string, 1)
202 202
 	}
203
-	for i := 0; i < len(commonFlags.Hosts); i++ {
203
+	for i := 0; i < len(cli.Config.Hosts); i++ {
204 204
 		var err error
205
-		if commonFlags.Hosts[i], err = opts.ParseHost(defaultHost, commonFlags.Hosts[i]); err != nil {
206
-			logrus.Fatalf("error parsing -H %s : %v", commonFlags.Hosts[i], err)
205
+		if cli.Config.Hosts[i], err = opts.ParseHost(defaultHost, cli.Config.Hosts[i]); err != nil {
206
+			logrus.Fatalf("error parsing -H %s : %v", cli.Config.Hosts[i], err)
207 207
 		}
208
-	}
209
-	for _, protoAddr := range commonFlags.Hosts {
208
+
209
+		protoAddr := cli.Config.Hosts[i]
210 210
 		protoAddrParts := strings.SplitN(protoAddr, "://", 2)
211 211
 		if len(protoAddrParts) != 2 {
212 212
 			logrus.Fatalf("bad format %s, expected PROTO://ADDR", protoAddr)
213 213
 		}
214 214
 		serverConfig.Addrs = append(serverConfig.Addrs, apiserver.Addr{Proto: protoAddrParts[0], Addr: protoAddrParts[1]})
215 215
 	}
216
+
216 217
 	api, err := apiserver.New(serverConfig)
217 218
 	if err != nil {
218 219
 		logrus.Fatal(err)
... ...
@@ -245,18 +270,21 @@ func (cli *DaemonCli) CmdDaemon(args ...string) error {
245 245
 
246 246
 	api.InitRouters(d)
247 247
 
248
+	reload := func(config *daemon.Config) {
249
+		if err := d.Reload(config); err != nil {
250
+			logrus.Errorf("Error reconfiguring the daemon: %v", err)
251
+			return
252
+		}
253
+		api.Reload(config)
254
+	}
255
+
256
+	setupConfigReloadTrap(*configFile, cli.flags, reload)
257
+
248 258
 	// The serve API routine never exits unless an error occurs
249 259
 	// We need to start it as a goroutine and wait on it so
250 260
 	// daemon doesn't exit
251 261
 	serveAPIWait := make(chan error)
252
-	go func() {
253
-		if err := api.ServeAPI(); err != nil {
254
-			logrus.Errorf("ServeAPI error: %v", err)
255
-			serveAPIWait <- err
256
-			return
257
-		}
258
-		serveAPIWait <- nil
259
-	}()
262
+	go api.Wait(serveAPIWait)
260 263
 
261 264
 	signal.Trap(func() {
262 265
 		api.Close()
... ...
@@ -303,3 +331,34 @@ func shutdownDaemon(d *daemon.Daemon, timeout time.Duration) {
303 303
 		logrus.Error("Force shutdown daemon")
304 304
 	}
305 305
 }
306
+
307
+func loadDaemonCliConfig(config *daemon.Config, daemonFlags *flag.FlagSet, commonConfig *cli.CommonFlags, configFile string) (*daemon.Config, error) {
308
+	config.Debug = commonConfig.Debug
309
+	config.Hosts = commonConfig.Hosts
310
+	config.LogLevel = commonConfig.LogLevel
311
+	config.TLS = commonConfig.TLS
312
+	config.TLSVerify = commonConfig.TLSVerify
313
+	config.TLSOptions = daemon.CommonTLSOptions{}
314
+
315
+	if commonConfig.TLSOptions != nil {
316
+		config.TLSOptions.CAFile = commonConfig.TLSOptions.CAFile
317
+		config.TLSOptions.CertFile = commonConfig.TLSOptions.CertFile
318
+		config.TLSOptions.KeyFile = commonConfig.TLSOptions.KeyFile
319
+	}
320
+
321
+	if configFile != "" {
322
+		c, err := daemon.MergeDaemonConfigurations(config, daemonFlags, configFile)
323
+		if err != nil {
324
+			if daemonFlags.IsSet(daemonConfigFileFlag) || !os.IsNotExist(err) {
325
+				return nil, fmt.Errorf("unable to configure the Docker daemon with file %s: %v\n", configFile, err)
326
+			}
327
+		}
328
+		// the merged configuration can be nil if the config file didn't exist.
329
+		// leave the current configuration as it is if when that happens.
330
+		if c != nil {
331
+			config = c
332
+		}
333
+	}
334
+
335
+	return config, nil
336
+}
306 337
new file mode 100644
... ...
@@ -0,0 +1,91 @@
0
+// +build daemon
1
+
2
+package main
3
+
4
+import (
5
+	"io/ioutil"
6
+	"strings"
7
+	"testing"
8
+
9
+	"github.com/docker/docker/cli"
10
+	"github.com/docker/docker/daemon"
11
+	"github.com/docker/docker/opts"
12
+	"github.com/docker/docker/pkg/mflag"
13
+	"github.com/docker/go-connections/tlsconfig"
14
+)
15
+
16
+func TestLoadDaemonCliConfigWithoutOverriding(t *testing.T) {
17
+	c := &daemon.Config{}
18
+	common := &cli.CommonFlags{
19
+		Debug: true,
20
+	}
21
+
22
+	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
23
+	loadedConfig, err := loadDaemonCliConfig(c, flags, common, "/tmp/fooobarbaz")
24
+	if err != nil {
25
+		t.Fatal(err)
26
+	}
27
+	if loadedConfig == nil {
28
+		t.Fatalf("expected configuration %v, got nil", c)
29
+	}
30
+	if !loadedConfig.Debug {
31
+		t.Fatalf("expected debug to be copied from the common flags, got false")
32
+	}
33
+}
34
+
35
+func TestLoadDaemonCliConfigWithTLS(t *testing.T) {
36
+	c := &daemon.Config{}
37
+	common := &cli.CommonFlags{
38
+		TLS: true,
39
+		TLSOptions: &tlsconfig.Options{
40
+			CAFile: "/tmp/ca.pem",
41
+		},
42
+	}
43
+
44
+	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
45
+	loadedConfig, err := loadDaemonCliConfig(c, flags, common, "/tmp/fooobarbaz")
46
+	if err != nil {
47
+		t.Fatal(err)
48
+	}
49
+	if loadedConfig == nil {
50
+		t.Fatalf("expected configuration %v, got nil", c)
51
+	}
52
+	if loadedConfig.TLSOptions.CAFile != "/tmp/ca.pem" {
53
+		t.Fatalf("expected /tmp/ca.pem, got %s: %q", loadedConfig.TLSOptions.CAFile, loadedConfig)
54
+	}
55
+}
56
+
57
+func TestLoadDaemonCliConfigWithConflicts(t *testing.T) {
58
+	c := &daemon.Config{}
59
+	common := &cli.CommonFlags{}
60
+	f, err := ioutil.TempFile("", "docker-config-")
61
+	if err != nil {
62
+		t.Fatal(err)
63
+	}
64
+
65
+	configFile := f.Name()
66
+	f.Write([]byte(`{"labels": ["l3=foo"]}`))
67
+	f.Close()
68
+
69
+	var labels []string
70
+
71
+	flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
72
+	flags.String([]string{daemonConfigFileFlag}, "", "")
73
+	flags.Var(opts.NewNamedListOptsRef("labels", &labels, opts.ValidateLabel), []string{"-label"}, "")
74
+
75
+	flags.Set(daemonConfigFileFlag, configFile)
76
+	if err := flags.Set("-label", "l1=bar"); err != nil {
77
+		t.Fatal(err)
78
+	}
79
+	if err := flags.Set("-label", "l2=baz"); err != nil {
80
+		t.Fatal(err)
81
+	}
82
+
83
+	_, err = loadDaemonCliConfig(c, flags, common, configFile)
84
+	if err == nil {
85
+		t.Fatalf("expected configuration error, got nil")
86
+	}
87
+	if !strings.Contains(err.Error(), "labels") {
88
+		t.Fatalf("expected labels conflict, got %v", err)
89
+	}
90
+}
... ...
@@ -5,15 +5,19 @@ package main
5 5
 import (
6 6
 	"fmt"
7 7
 	"os"
8
+	"os/signal"
8 9
 	"syscall"
9 10
 
10 11
 	apiserver "github.com/docker/docker/api/server"
11 12
 	"github.com/docker/docker/daemon"
13
+	"github.com/docker/docker/pkg/mflag"
12 14
 	"github.com/docker/docker/pkg/system"
13 15
 
14 16
 	_ "github.com/docker/docker/daemon/execdriver/native"
15 17
 )
16 18
 
19
+const defaultDaemonConfigFile = "/etc/docker/daemon.json"
20
+
17 21
 func setPlatformServerConfig(serverConfig *apiserver.Config, daemonCfg *daemon.Config) *apiserver.Config {
18 22
 	serverConfig.SocketGroup = daemonCfg.SocketGroup
19 23
 	serverConfig.EnableCors = daemonCfg.EnableCors
... ...
@@ -48,3 +52,14 @@ func setDefaultUmask() error {
48 48
 func getDaemonConfDir() string {
49 49
 	return "/etc/docker"
50 50
 }
51
+
52
+// setupConfigReloadTrap configures the USR2 signal to reload the configuration.
53
+func setupConfigReloadTrap(configFile string, flags *mflag.FlagSet, reload func(*daemon.Config)) {
54
+	c := make(chan os.Signal, 1)
55
+	signal.Notify(c, syscall.SIGHUP)
56
+	go func() {
57
+		for range c {
58
+			daemon.ReloadConfiguration(configFile, flags, reload)
59
+		}
60
+	}()
61
+}
... ...
@@ -3,12 +3,19 @@
3 3
 package main
4 4
 
5 5
 import (
6
+	"fmt"
6 7
 	"os"
8
+	"syscall"
7 9
 
10
+	"github.com/Sirupsen/logrus"
8 11
 	apiserver "github.com/docker/docker/api/server"
9 12
 	"github.com/docker/docker/daemon"
13
+	"github.com/docker/docker/pkg/mflag"
14
+	"github.com/docker/docker/pkg/system"
10 15
 )
11 16
 
17
+var defaultDaemonConfigFile = os.Getenv("programdata") + string(os.PathSeparator) + "docker" + string(os.PathSeparator) + "config" + string(os.PathSeparator) + "daemon.json"
18
+
12 19
 func setPlatformServerConfig(serverConfig *apiserver.Config, daemonCfg *daemon.Config) *apiserver.Config {
13 20
 	return serverConfig
14 21
 }
... ...
@@ -31,3 +38,20 @@ func getDaemonConfDir() string {
31 31
 // notifySystem sends a message to the host when the server is ready to be used
32 32
 func notifySystem() {
33 33
 }
34
+
35
+// setupConfigReloadTrap configures a Win32 event to reload the configuration.
36
+func setupConfigReloadTrap(configFile string, flags *mflag.FlagSet, reload func(*daemon.Config)) {
37
+	go func() {
38
+		sa := syscall.SecurityAttributes{
39
+			Length: 0,
40
+		}
41
+		ev := "Global\\docker-daemon-config-" + fmt.Sprint(os.Getpid())
42
+		if h, _ := system.CreateEvent(&sa, false, false, ev); h != 0 {
43
+			logrus.Debugf("Config reload - waiting signal at %s", ev)
44
+			for {
45
+				syscall.WaitForSingleObject(h, syscall.INFINITE)
46
+				daemon.ReloadConfiguration(configFile, flags, reload)
47
+			}
48
+		}
49
+	}()
50
+}
... ...
@@ -27,6 +27,7 @@ weight = -1
27 27
       --cluster-store=""                     URL of the distributed storage backend
28 28
       --cluster-advertise=""                 Address of the daemon instance on the cluster
29 29
       --cluster-store-opt=map[]              Set cluster options
30
+      --config-file=/etc/docker/daemon.json  Daemon configuration file
30 31
       --dns=[]                               DNS server to use
31 32
       --dns-opt=[]                           DNS options to use
32 33
       --dns-search=[]                        DNS search domains to use
... ...
@@ -776,7 +777,7 @@ set like this:
776 776
     /usr/local/bin/docker daemon -D -g /var/lib/docker -H unix:// > /var/lib/docker-machine/docker.log 2>&1
777 777
 
778 778
 
779
-# Default cgroup parent
779
+## Default cgroup parent
780 780
 
781 781
 The `--cgroup-parent` option allows you to set the default cgroup parent
782 782
 to use for containers. If this option is not set, it defaults to `/docker` for
... ...
@@ -794,3 +795,79 @@ creates the cgroup in `/sys/fs/cgroup/memory/daemoncgroup/foobar`
794 794
 This setting can also be set per container, using the `--cgroup-parent`
795 795
 option on `docker create` and `docker run`, and takes precedence over
796 796
 the `--cgroup-parent` option on the daemon.
797
+
798
+## Daemon configuration file
799
+
800
+The `--config-file` option allows you to set any configuration option
801
+for the daemon in a JSON format. This file uses the same flag names as keys,
802
+except for flags that allow several entries, where it uses the plural
803
+of the flag name, e.g., `labels` for the `label` flag. By default,
804
+docker tries to load a configuration file from `/etc/docker/daemon.json`
805
+on Linux and `%programdata%\docker\config\daemon.json` on Windows.
806
+
807
+The options set in the configuration file must not conflict with options set
808
+via flags. The docker daemon fails to start if an option is duplicated between
809
+the file and the flags, regardless their value. We do this to avoid
810
+silently ignore changes introduced in configuration reloads.
811
+For example, the daemon fails to start if you set daemon labels
812
+in the configuration file and also set daemon labels via the `--label` flag.
813
+
814
+Options that are not present in the file are ignored when the daemon starts.
815
+This is a full example of the allowed configuration options in the file:
816
+
817
+```json
818
+{
819
+	"authorization-plugins": [],
820
+	"dns": [],
821
+	"dns-opts": [],
822
+	"dns-search": [],
823
+	"exec-opts": [],
824
+	"exec-root": "",
825
+	"storage-driver": "",
826
+	"storage-opts": "",
827
+	"labels": [],
828
+	"log-config": {
829
+		"log-driver": "",
830
+		"log-opts": []
831
+	},
832
+	"mtu": 0,
833
+	"pidfile": "",
834
+	"graph": "",
835
+	"cluster-store": "",
836
+	"cluster-store-opts": [],
837
+	"cluster-advertise": "",
838
+	"debug": true,
839
+	"hosts": [],
840
+	"log-level": "",
841
+	"tls": true,
842
+	"tls-verify": true,
843
+	"tls-opts": {
844
+		"tlscacert": "",
845
+		"tlscert": "",
846
+		"tlskey": ""
847
+	},
848
+	"api-cors-headers": "",
849
+	"selinux-enabled": false,
850
+	"userns-remap": "",
851
+	"group": "",
852
+	"cgroup-parent": "",
853
+	"default-ulimits": {}
854
+}
855
+```
856
+
857
+### Configuration reloading
858
+
859
+Some options can be reconfigured when the daemon is running without requiring
860
+to restart the process. We use the `SIGHUP` signal in Linux to reload, and a global event
861
+in Windows with the key `Global\docker-daemon-config-$PID`. The options can
862
+be modified in the configuration file but still will check for conflicts with
863
+the provided flags. The daemon fails to reconfigure itself
864
+if there are conflicts, but it won't stop execution.
865
+
866
+The list of currently supported options that can be reconfigured is this:
867
+
868
+- `debug`: it changes the daemon to debug mode when set to true.
869
+- `label`: it replaces the daemon labels with a new set of labels.
870
+- `cluster-store`: it reloads the discovery store with the new address.
871
+- `cluster-store-opts`: it uses the new options to reload the discovery store.
872
+- `cluster-advertise`: it modifies the address advertised after reloading.
... ...
@@ -133,7 +133,7 @@ func (s *DockerSuite) TestHelpTextVerify(c *check.C) {
133 133
 			// Check each line for lots of stuff
134 134
 			lines := strings.Split(out, "\n")
135 135
 			for _, line := range lines {
136
-				c.Assert(len(line), checker.LessOrEqualThan, 103, check.Commentf("Help for %q is too long:\n%s", cmd, line))
136
+				c.Assert(len(line), checker.LessOrEqualThan, 107, check.Commentf("Help for %q is too long:\n%s", cmd, line))
137 137
 
138 138
 				if scanForHome && strings.Contains(line, `"`+home) {
139 139
 					c.Fatalf("Help for %q should use ~ instead of %q on:\n%s",
... ...
@@ -14,6 +14,7 @@ docker-daemon - Enable daemon mode
14 14
 [**--cluster-store**[=*[]*]]
15 15
 [**--cluster-advertise**[=*[]*]]
16 16
 [**--cluster-store-opt**[=*map[]*]]
17
+[**--config-file**[=*/etc/docker/daemon.json*]]
17 18
 [**-D**|**--debug**]
18 19
 [**--default-gateway**[=*DEFAULT-GATEWAY*]]
19 20
 [**--default-gateway-v6**[=*DEFAULT-GATEWAY-V6*]]
... ...
@@ -96,6 +97,9 @@ format.
96 96
 **--cluster-store-opt**=""
97 97
   Specifies options for the Key/Value store.
98 98
 
99
+**--config-file**="/etc/docker/daemon.json"
100
+  Specifies the JSON file path to load the configuration from.
101
+
99 102
 **-D**, **--debug**=*true*|*false*
100 103
   Enable debug mode. Default is false.
101 104
 
... ...
@@ -100,6 +100,35 @@ func (opts *ListOpts) Len() int {
100 100
 	return len((*opts.values))
101 101
 }
102 102
 
103
+// NamedOption is an interface that list and map options
104
+// with names implement.
105
+type NamedOption interface {
106
+	Name() string
107
+}
108
+
109
+// NamedListOpts is a ListOpts with a configuration name.
110
+// This struct is useful to keep reference to the assigned
111
+// field name in the internal configuration struct.
112
+type NamedListOpts struct {
113
+	name string
114
+	ListOpts
115
+}
116
+
117
+var _ NamedOption = &NamedListOpts{}
118
+
119
+// NewNamedListOptsRef creates a reference to a new NamedListOpts struct.
120
+func NewNamedListOptsRef(name string, values *[]string, validator ValidatorFctType) *NamedListOpts {
121
+	return &NamedListOpts{
122
+		name:     name,
123
+		ListOpts: *NewListOptsRef(values, validator),
124
+	}
125
+}
126
+
127
+// Name returns the name of the NamedListOpts in the configuration.
128
+func (o *NamedListOpts) Name() string {
129
+	return o.name
130
+}
131
+
103 132
 //MapOpts holds a map of values and a validation function.
104 133
 type MapOpts struct {
105 134
 	values    map[string]string
... ...
@@ -145,6 +174,29 @@ func NewMapOpts(values map[string]string, validator ValidatorFctType) *MapOpts {
145 145
 	}
146 146
 }
147 147
 
148
+// NamedMapOpts is a MapOpts struct with a configuration name.
149
+// This struct is useful to keep reference to the assigned
150
+// field name in the internal configuration struct.
151
+type NamedMapOpts struct {
152
+	name string
153
+	MapOpts
154
+}
155
+
156
+var _ NamedOption = &NamedMapOpts{}
157
+
158
+// NewNamedMapOpts creates a reference to a new NamedMapOpts struct.
159
+func NewNamedMapOpts(name string, values map[string]string, validator ValidatorFctType) *NamedMapOpts {
160
+	return &NamedMapOpts{
161
+		name:    name,
162
+		MapOpts: *NewMapOpts(values, validator),
163
+	}
164
+}
165
+
166
+// Name returns the name of the NamedMapOpts in the configuration.
167
+func (o *NamedMapOpts) Name() string {
168
+	return o.name
169
+}
170
+
148 171
 // ValidatorFctType defines a validator function that returns a validated string and/or an error.
149 172
 type ValidatorFctType func(val string) (string, error)
150 173
 
... ...
@@ -198,3 +198,35 @@ func logOptsValidator(val string) (string, error) {
198 198
 	}
199 199
 	return "", fmt.Errorf("invalid key %s", vals[0])
200 200
 }
201
+
202
+func TestNamedListOpts(t *testing.T) {
203
+	var v []string
204
+	o := NewNamedListOptsRef("foo-name", &v, nil)
205
+
206
+	o.Set("foo")
207
+	if o.String() != "[foo]" {
208
+		t.Errorf("%s != [foo]", o.String())
209
+	}
210
+	if o.Name() != "foo-name" {
211
+		t.Errorf("%s != foo-name", o.Name())
212
+	}
213
+	if len(v) != 1 {
214
+		t.Errorf("expected foo to be in the values, got %v", v)
215
+	}
216
+}
217
+
218
+func TestNamedMapOpts(t *testing.T) {
219
+	tmpMap := make(map[string]string)
220
+	o := NewNamedMapOpts("max-name", tmpMap, nil)
221
+
222
+	o.Set("max-size=1")
223
+	if o.String() != "map[max-size:1]" {
224
+		t.Errorf("%s != [map[max-size:1]", o.String())
225
+	}
226
+	if o.Name() != "max-name" {
227
+		t.Errorf("%s != max-name", o.Name())
228
+	}
229
+	if _, exist := tmpMap["max-size"]; !exist {
230
+		t.Errorf("expected map-size to be in the values, got %v", tmpMap)
231
+	}
232
+}
... ...
@@ -12,12 +12,8 @@ import (
12 12
 var (
13 13
 	// Backends is a global map of discovery backends indexed by their
14 14
 	// associated scheme.
15
-	backends map[string]Backend
16
-)
17
-
18
-func init() {
19 15
 	backends = make(map[string]Backend)
20
-}
16
+)
21 17
 
22 18
 // Register makes a discovery backend available by the provided scheme.
23 19
 // If Register is called twice with the same scheme an error is returned.
... ...
@@ -42,7 +38,7 @@ func parse(rawurl string) (string, string) {
42 42
 
43 43
 // ParseAdvertise parses the --cluster-advertise daemon config which accepts
44 44
 // <ip-address>:<port> or <interface-name>:<port>
45
-func ParseAdvertise(store, advertise string) (string, error) {
45
+func ParseAdvertise(advertise string) (string, error) {
46 46
 	var (
47 47
 		iface *net.Interface
48 48
 		addrs []net.Addr
... ...
@@ -25,6 +25,7 @@ func Init() {
25 25
 // Initialize sets the heartbeat for the memory backend.
26 26
 func (s *Discovery) Initialize(_ string, heartbeat time.Duration, _ time.Duration, _ map[string]string) error {
27 27
 	s.heartbeat = heartbeat
28
+	s.values = make([]string, 0)
28 29
 	return nil
29 30
 }
30 31
 
31 32
new file mode 100644
... ...
@@ -0,0 +1,26 @@
0
+package utils
1
+
2
+import (
3
+	"os"
4
+
5
+	"github.com/Sirupsen/logrus"
6
+)
7
+
8
+// EnableDebug sets the DEBUG env var to true
9
+// and makes the logger to log at debug level.
10
+func EnableDebug() {
11
+	os.Setenv("DEBUG", "1")
12
+	logrus.SetLevel(logrus.DebugLevel)
13
+}
14
+
15
+// DisableDebug sets the DEBUG env var to false
16
+// and makes the logger to log at info level.
17
+func DisableDebug() {
18
+	os.Setenv("DEBUG", "")
19
+	logrus.SetLevel(logrus.InfoLevel)
20
+}
21
+
22
+// IsDebugEnabled checks whether the debug flag is set or not.
23
+func IsDebugEnabled() bool {
24
+	return os.Getenv("DEBUG") != ""
25
+}