Skip to content

Commit 677a6b3

Browse files
committed
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 <[email protected]>
1 parent 22a81a2 commit 677a6b3

23 files changed

Lines changed: 1217 additions & 127 deletions

api/server/router_swapper.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package server
2+
3+
import (
4+
"net/http"
5+
"sync"
6+
7+
"github.com/gorilla/mux"
8+
)
9+
10+
// routerSwapper is an http.Handler that allow you to swap
11+
// mux routers.
12+
type routerSwapper struct {
13+
mu sync.Mutex
14+
router *mux.Router
15+
}
16+
17+
// Swap changes the old router with the new one.
18+
func (rs *routerSwapper) Swap(newRouter *mux.Router) {
19+
rs.mu.Lock()
20+
rs.router = newRouter
21+
rs.mu.Unlock()
22+
}
23+
24+
// ServeHTTP makes the routerSwapper to implement the http.Handler interface.
25+
func (rs *routerSwapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
26+
rs.mu.Lock()
27+
router := rs.router
28+
rs.mu.Unlock()
29+
router.ServeHTTP(w, r)
30+
}

api/server/server.go

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"crypto/tls"
55
"net"
66
"net/http"
7-
"os"
87
"strings"
98

109
"github.com/Sirupsen/logrus"
@@ -42,10 +41,11 @@ type Config struct {
4241

4342
// Server contains instance details for the server
4443
type Server struct {
45-
cfg *Config
46-
servers []*HTTPServer
47-
routers []router.Router
48-
authZPlugins []authorization.Plugin
44+
cfg *Config
45+
servers []*HTTPServer
46+
routers []router.Router
47+
authZPlugins []authorization.Plugin
48+
routerSwapper *routerSwapper
4949
}
5050

5151
// Addr contains string representation of address and its protocol (tcp, unix...).
@@ -80,12 +80,14 @@ func (s *Server) Close() {
8080
}
8181
}
8282

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+
8688
var chErrors = make(chan error, len(s.servers))
8789
for _, srv := range s.servers {
88-
srv.srv.Handler = s.CreateMux()
90+
srv.srv.Handler = s.routerSwapper
8991
go func(srv *HTTPServer) {
9092
var err error
9193
logrus.Infof("API listen on %s", srv.l.Addr())
@@ -186,11 +188,11 @@ func (s *Server) addRouter(r router.Router) {
186188
s.routers = append(s.routers, r)
187189
}
188190

189-
// CreateMux initializes the main router the server uses.
191+
// createMux initializes the main router the server uses.
190192
// we keep enableCors just for legacy usage, need to be removed in the future
191-
func (s *Server) CreateMux() *mux.Router {
193+
func (s *Server) createMux() *mux.Router {
192194
m := mux.NewRouter()
193-
if os.Getenv("DEBUG") != "" {
195+
if utils.IsDebugEnabled() {
194196
profilerSetup(m, "/debug/")
195197
}
196198

@@ -207,3 +209,36 @@ func (s *Server) CreateMux() *mux.Router {
207209

208210
return m
209211
}
212+
213+
// Wait blocks the server goroutine until it exits.
214+
// It sends an error message if there is any error during
215+
// the API execution.
216+
func (s *Server) Wait(waitChan chan error) {
217+
if err := s.serveAPI(); err != nil {
218+
logrus.Errorf("ServeAPI error: %v", err)
219+
waitChan <- err
220+
return
221+
}
222+
waitChan <- nil
223+
}
224+
225+
func (s *Server) initRouterSwapper() {
226+
s.routerSwapper = &routerSwapper{
227+
router: s.createMux(),
228+
}
229+
}
230+
231+
// Reload reads configuration changes and modifies the
232+
// server according to those changes.
233+
// Currently, only the --debug configuration is taken into account.
234+
func (s *Server) Reload(config *daemon.Config) {
235+
debugEnabled := utils.IsDebugEnabled()
236+
switch {
237+
case debugEnabled && !config.Debug: // disable debug
238+
utils.DisableDebug()
239+
s.routerSwapper.Swap(s.createMux())
240+
case config.Debug && !debugEnabled: // enable debug
241+
utils.EnableDebug()
242+
s.routerSwapper.Swap(s.createMux())
243+
}
244+
}

daemon/config.go

Lines changed: 186 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,99 @@
11
package daemon
22

33
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"io/ioutil"
9+
"strings"
10+
"sync"
11+
12+
"github.com/Sirupsen/logrus"
413
"github.com/docker/docker/opts"
14+
"github.com/docker/docker/pkg/discovery"
515
flag "github.com/docker/docker/pkg/mflag"
6-
"github.com/docker/engine-api/types/container"
16+
"github.com/imdario/mergo"
717
)
818

919
const (
1020
defaultNetworkMtu = 1500
1121
disableNetworkBridge = "none"
1222
)
1323

24+
// LogConfig represents the default log configuration.
25+
// It includes json tags to deserialize configuration from a file
26+
// using the same names that the flags in the command line uses.
27+
type LogConfig struct {
28+
Type string `json:"log-driver,omitempty"`
29+
Config map[string]string `json:"log-opts,omitempty"`
30+
}
31+
32+
// CommonTLSOptions defines TLS configuration for the daemon server.
33+
// It includes json tags to deserialize configuration from a file
34+
// using the same names that the flags in the command line uses.
35+
type CommonTLSOptions struct {
36+
CAFile string `json:"tlscacert,omitempty"`
37+
CertFile string `json:"tlscert,omitempty"`
38+
KeyFile string `json:"tlskey,omitempty"`
39+
}
40+
1441
// CommonConfig defines the configuration of a docker daemon which are
1542
// common across platforms.
43+
// It includes json tags to deserialize configuration from a file
44+
// using the same names that the flags in the command line uses.
1645
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
46+
AuthorizationPlugins []string `json:"authorization-plugins,omitempty"` // AuthorizationPlugins holds list of authorization plugins
47+
AutoRestart bool `json:"-"`
48+
Bridge bridgeConfig `json:"-"` // Bridge holds bridge network specific configuration.
49+
Context map[string][]string `json:"-"`
50+
DisableBridge bool `json:"-"`
51+
DNS []string `json:"dns,omitempty"`
52+
DNSOptions []string `json:"dns-opts,omitempty"`
53+
DNSSearch []string `json:"dns-search,omitempty"`
54+
ExecOptions []string `json:"exec-opts,omitempty"`
55+
ExecRoot string `json:"exec-root,omitempty"`
56+
GraphDriver string `json:"storage-driver,omitempty"`
57+
GraphOptions []string `json:"storage-opts,omitempty"`
58+
Labels []string `json:"labels,omitempty"`
59+
LogConfig LogConfig `json:"log-config,omitempty"`
60+
Mtu int `json:"mtu,omitempty"`
61+
Pidfile string `json:"pidfile,omitempty"`
62+
Root string `json:"graph,omitempty"`
63+
TrustKeyPath string `json:"-"`
3664

3765
// ClusterStore is the storage backend used for the cluster information. It is used by both
3866
// multihost networking (to store networks and endpoints information) and by the node discovery
3967
// mechanism.
40-
ClusterStore string
68+
ClusterStore string `json:"cluster-store,omitempty"`
4169

4270
// ClusterOpts is used to pass options to the discovery package for tuning libkv settings, such
4371
// as TLS configuration settings.
44-
ClusterOpts map[string]string
72+
ClusterOpts map[string]string `json:"cluster-store-opts,omitempty"`
4573

4674
// ClusterAdvertise is the network endpoint that the Engine advertises for the purpose of node
4775
// discovery. This should be a 'host:port' combination on which that daemon instance is
4876
// reachable by other hosts.
49-
ClusterAdvertise string
77+
ClusterAdvertise string `json:"cluster-advertise,omitempty"`
78+
79+
Debug bool `json:"debug,omitempty"`
80+
Hosts []string `json:"hosts,omitempty"`
81+
LogLevel string `json:"log-level,omitempty"`
82+
TLS bool `json:"tls,omitempty"`
83+
TLSVerify bool `json:"tls-verify,omitempty"`
84+
TLSOptions CommonTLSOptions `json:"tls-opts,omitempty"`
85+
86+
reloadLock sync.Mutex
5087
}
5188

5289
// InstallCommonFlags adds command-line options to the top-level flag parser for
5390
// the current process.
5491
// Subsequent calls to `flag.Parse` will populate config with values parsed
5592
// from the command-line.
5693
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"))
94+
cmd.Var(opts.NewNamedListOptsRef("storage-opts", &config.GraphOptions, nil), []string{"-storage-opt"}, usageFn("Set storage driver options"))
95+
cmd.Var(opts.NewNamedListOptsRef("authorization-plugins", &config.AuthorizationPlugins, nil), []string{"-authorization-plugin"}, usageFn("List authorization plugins in order from first evaluator to last"))
96+
cmd.Var(opts.NewNamedListOptsRef("exec-opts", &config.ExecOptions, nil), []string{"-exec-opt"}, usageFn("Set exec driver options"))
6097
cmd.StringVar(&config.Pidfile, []string{"p", "-pidfile"}, defaultPidFile, usageFn("Path to use for daemon PID file"))
6198
cmd.StringVar(&config.Root, []string{"g", "-graph"}, defaultGraph, usageFn("Root of the Docker runtime"))
6299
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)
65102
cmd.IntVar(&config.Mtu, []string{"#mtu", "-mtu"}, 0, usageFn("Set the containers network MTU"))
66103
// FIXME: why the inconsistency between "hosts" and "sockets"?
67104
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"))
105+
cmd.Var(opts.NewNamedListOptsRef("dns-opts", &config.DNSOptions, nil), []string{"-dns-opt"}, usageFn("DNS options to use"))
69106
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"))
107+
cmd.Var(opts.NewNamedListOptsRef("labels", &config.Labels, opts.ValidateLabel), []string{"-label"}, usageFn("Set key=value labels to the daemon"))
71108
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"))
109+
cmd.Var(opts.NewNamedMapOpts("log-opts", config.LogConfig.Config, nil), []string{"-log-opt"}, usageFn("Set log driver options"))
73110
cmd.StringVar(&config.ClusterAdvertise, []string{"-cluster-advertise"}, "", usageFn("Address or interface name to advertise"))
74111
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"))
112+
cmd.Var(opts.NewNamedMapOpts("cluster-store-opts", config.ClusterOpts, nil), []string{"-cluster-store-opt"}, usageFn("Set cluster store options"))
113+
}
114+
115+
func parseClusterAdvertiseSettings(clusterStore, clusterAdvertise string) (string, error) {
116+
if clusterAdvertise == "" {
117+
return "", errDiscoveryDisabled
118+
}
119+
if clusterStore == "" {
120+
return "", fmt.Errorf("invalid cluster configuration. --cluster-advertise must be accompanied by --cluster-store configuration")
121+
}
122+
123+
advertise, err := discovery.ParseAdvertise(clusterAdvertise)
124+
if err != nil {
125+
return "", fmt.Errorf("discovery advertise parsing failed (%v)", err)
126+
}
127+
return advertise, nil
128+
}
129+
130+
// ReloadConfiguration reads the configuration in the host and reloads the daemon and server.
131+
func ReloadConfiguration(configFile string, flags *flag.FlagSet, reload func(*Config)) {
132+
logrus.Infof("Got signal to reload configuration, reloading from: %s", configFile)
133+
newConfig, err := getConflictFreeConfiguration(configFile, flags)
134+
if err != nil {
135+
logrus.Error(err)
136+
} else {
137+
reload(newConfig)
138+
}
139+
}
140+
141+
// MergeDaemonConfigurations reads a configuration file,
142+
// loads the file configuration in an isolated structure,
143+
// and merges the configuration provided from flags on top
144+
// if there are no conflicts.
145+
func MergeDaemonConfigurations(flagsConfig *Config, flags *flag.FlagSet, configFile string) (*Config, error) {
146+
fileConfig, err := getConflictFreeConfiguration(configFile, flags)
147+
if err != nil {
148+
return nil, err
149+
}
150+
151+
// merge flags configuration on top of the file configuration
152+
if err := mergo.Merge(fileConfig, flagsConfig); err != nil {
153+
return nil, err
154+
}
155+
156+
return fileConfig, nil
157+
}
158+
159+
// getConflictFreeConfiguration loads the configuration from a JSON file.
160+
// It compares that configuration with the one provided by the flags,
161+
// and returns an error if there are conflicts.
162+
func getConflictFreeConfiguration(configFile string, flags *flag.FlagSet) (*Config, error) {
163+
b, err := ioutil.ReadFile(configFile)
164+
if err != nil {
165+
return nil, err
166+
}
167+
168+
var reader io.Reader
169+
if flags != nil {
170+
var jsonConfig map[string]interface{}
171+
reader = bytes.NewReader(b)
172+
if err := json.NewDecoder(reader).Decode(&jsonConfig); err != nil {
173+
return nil, err
174+
}
175+
176+
if err := findConfigurationConflicts(jsonConfig, flags); err != nil {
177+
return nil, err
178+
}
179+
}
180+
181+
var config Config
182+
reader = bytes.NewReader(b)
183+
err = json.NewDecoder(reader).Decode(&config)
184+
return &config, err
185+
}
186+
187+
// findConfigurationConflicts iterates over the provided flags searching for
188+
// duplicated configurations. It returns an error with all the conflicts if
189+
// it finds any.
190+
func findConfigurationConflicts(config map[string]interface{}, flags *flag.FlagSet) error {
191+
var conflicts []string
192+
flatten := make(map[string]interface{})
193+
for k, v := range config {
194+
if m, ok := v.(map[string]interface{}); ok {
195+
for km, vm := range m {
196+
flatten[km] = vm
197+
}
198+
} else {
199+
flatten[k] = v
200+
}
201+
}
202+
203+
printConflict := func(name string, flagValue, fileValue interface{}) string {
204+
return fmt.Sprintf("%s: (from flag: %v, from file: %v)", name, flagValue, fileValue)
205+
}
206+
207+
collectConflicts := func(f *flag.Flag) {
208+
// search option name in the json configuration payload if the value is a named option
209+
if namedOption, ok := f.Value.(opts.NamedOption); ok {
210+
if optsValue, ok := flatten[namedOption.Name()]; ok {
211+
conflicts = append(conflicts, printConflict(namedOption.Name(), f.Value.String(), optsValue))
212+
}
213+
} else {
214+
// search flag name in the json configuration payload without trailing dashes
215+
for _, name := range f.Names {
216+
name = strings.TrimLeft(name, "-")
217+
218+
if value, ok := flatten[name]; ok {
219+
conflicts = append(conflicts, printConflict(name, f.Value.String(), value))
220+
break
221+
}
222+
}
223+
}
224+
}
225+
226+
flags.Visit(collectConflicts)
227+
228+
if len(conflicts) > 0 {
229+
return fmt.Errorf("the following directives are specified both as a flag and in the configuration file: %s", strings.Join(conflicts, ", "))
230+
}
231+
return nil
76232
}

0 commit comments

Comments
 (0)