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>
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 |
+} |