Allow to set daemon and server configurations in a file.

Read configuration after flags making this the priority:

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

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

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

Signed-off-by: David Calavera <david.calavera@gmail.com>
This commit is contained in:
David Calavera 2015-12-10 18:35:10 -05:00
parent 22a81a2c58
commit 677a6b3506
23 changed files with 1218 additions and 128 deletions

View File

@ -0,0 +1,30 @@
package server
import (
"net/http"
"sync"
"github.com/gorilla/mux"
)
// routerSwapper is an http.Handler that allow you to swap
// mux routers.
type routerSwapper struct {
mu sync.Mutex
router *mux.Router
}
// Swap changes the old router with the new one.
func (rs *routerSwapper) Swap(newRouter *mux.Router) {
rs.mu.Lock()
rs.router = newRouter
rs.mu.Unlock()
}
// ServeHTTP makes the routerSwapper to implement the http.Handler interface.
func (rs *routerSwapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rs.mu.Lock()
router := rs.router
rs.mu.Unlock()
router.ServeHTTP(w, r)
}

View File

@ -4,7 +4,6 @@ import (
"crypto/tls" "crypto/tls"
"net" "net"
"net/http" "net/http"
"os"
"strings" "strings"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
@ -42,10 +41,11 @@ type Config struct {
// Server contains instance details for the server // Server contains instance details for the server
type Server struct { type Server struct {
cfg *Config cfg *Config
servers []*HTTPServer servers []*HTTPServer
routers []router.Router routers []router.Router
authZPlugins []authorization.Plugin authZPlugins []authorization.Plugin
routerSwapper *routerSwapper
} }
// Addr contains string representation of address and its protocol (tcp, unix...). // Addr contains string representation of address and its protocol (tcp, unix...).
@ -80,12 +80,14 @@ func (s *Server) Close() {
} }
} }
// ServeAPI loops through all initialized servers and spawns goroutine // serveAPI loops through all initialized servers and spawns goroutine
// with Server method for each. It sets CreateMux() as Handler also. // with Server method for each. It sets createMux() as Handler also.
func (s *Server) ServeAPI() error { func (s *Server) serveAPI() error {
s.initRouterSwapper()
var chErrors = make(chan error, len(s.servers)) var chErrors = make(chan error, len(s.servers))
for _, srv := range s.servers { for _, srv := range s.servers {
srv.srv.Handler = s.CreateMux() srv.srv.Handler = s.routerSwapper
go func(srv *HTTPServer) { go func(srv *HTTPServer) {
var err error var err error
logrus.Infof("API listen on %s", srv.l.Addr()) logrus.Infof("API listen on %s", srv.l.Addr())
@ -186,11 +188,11 @@ func (s *Server) addRouter(r router.Router) {
s.routers = append(s.routers, r) s.routers = append(s.routers, r)
} }
// CreateMux initializes the main router the server uses. // createMux initializes the main router the server uses.
// we keep enableCors just for legacy usage, need to be removed in the future // we keep enableCors just for legacy usage, need to be removed in the future
func (s *Server) CreateMux() *mux.Router { func (s *Server) createMux() *mux.Router {
m := mux.NewRouter() m := mux.NewRouter()
if os.Getenv("DEBUG") != "" { if utils.IsDebugEnabled() {
profilerSetup(m, "/debug/") profilerSetup(m, "/debug/")
} }
@ -207,3 +209,36 @@ func (s *Server) CreateMux() *mux.Router {
return m return m
} }
// Wait blocks the server goroutine until it exits.
// It sends an error message if there is any error during
// the API execution.
func (s *Server) Wait(waitChan chan error) {
if err := s.serveAPI(); err != nil {
logrus.Errorf("ServeAPI error: %v", err)
waitChan <- err
return
}
waitChan <- nil
}
func (s *Server) initRouterSwapper() {
s.routerSwapper = &routerSwapper{
router: s.createMux(),
}
}
// Reload reads configuration changes and modifies the
// server according to those changes.
// Currently, only the --debug configuration is taken into account.
func (s *Server) Reload(config *daemon.Config) {
debugEnabled := utils.IsDebugEnabled()
switch {
case debugEnabled && !config.Debug: // disable debug
utils.DisableDebug()
s.routerSwapper.Swap(s.createMux())
case config.Debug && !debugEnabled: // enable debug
utils.EnableDebug()
s.routerSwapper.Swap(s.createMux())
}
}

View File

@ -1,9 +1,19 @@
package daemon package daemon
import ( import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"strings"
"sync"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/opts" "github.com/docker/docker/opts"
"github.com/docker/docker/pkg/discovery"
flag "github.com/docker/docker/pkg/mflag" flag "github.com/docker/docker/pkg/mflag"
"github.com/docker/engine-api/types/container" "github.com/imdario/mergo"
) )
const ( const (
@ -11,42 +21,69 @@ const (
disableNetworkBridge = "none" disableNetworkBridge = "none"
) )
// LogConfig represents the default log configuration.
// It includes json tags to deserialize configuration from a file
// using the same names that the flags in the command line uses.
type LogConfig struct {
Type string `json:"log-driver,omitempty"`
Config map[string]string `json:"log-opts,omitempty"`
}
// CommonTLSOptions defines TLS configuration for the daemon server.
// It includes json tags to deserialize configuration from a file
// using the same names that the flags in the command line uses.
type CommonTLSOptions struct {
CAFile string `json:"tlscacert,omitempty"`
CertFile string `json:"tlscert,omitempty"`
KeyFile string `json:"tlskey,omitempty"`
}
// CommonConfig defines the configuration of a docker daemon which are // CommonConfig defines the configuration of a docker daemon which are
// common across platforms. // common across platforms.
// It includes json tags to deserialize configuration from a file
// using the same names that the flags in the command line uses.
type CommonConfig struct { type CommonConfig struct {
AuthorizationPlugins []string // AuthorizationPlugins holds list of authorization plugins AuthorizationPlugins []string `json:"authorization-plugins,omitempty"` // AuthorizationPlugins holds list of authorization plugins
AutoRestart bool AutoRestart bool `json:"-"`
Bridge bridgeConfig // Bridge holds bridge network specific configuration. Bridge bridgeConfig `json:"-"` // Bridge holds bridge network specific configuration.
Context map[string][]string Context map[string][]string `json:"-"`
DisableBridge bool DisableBridge bool `json:"-"`
DNS []string DNS []string `json:"dns,omitempty"`
DNSOptions []string DNSOptions []string `json:"dns-opts,omitempty"`
DNSSearch []string DNSSearch []string `json:"dns-search,omitempty"`
ExecOptions []string ExecOptions []string `json:"exec-opts,omitempty"`
ExecRoot string ExecRoot string `json:"exec-root,omitempty"`
GraphDriver string GraphDriver string `json:"storage-driver,omitempty"`
GraphOptions []string GraphOptions []string `json:"storage-opts,omitempty"`
Labels []string Labels []string `json:"labels,omitempty"`
LogConfig container.LogConfig LogConfig LogConfig `json:"log-config,omitempty"`
Mtu int Mtu int `json:"mtu,omitempty"`
Pidfile string Pidfile string `json:"pidfile,omitempty"`
RemappedRoot string Root string `json:"graph,omitempty"`
Root string TrustKeyPath string `json:"-"`
TrustKeyPath string
// ClusterStore is the storage backend used for the cluster information. It is used by both // ClusterStore is the storage backend used for the cluster information. It is used by both
// multihost networking (to store networks and endpoints information) and by the node discovery // multihost networking (to store networks and endpoints information) and by the node discovery
// mechanism. // mechanism.
ClusterStore string ClusterStore string `json:"cluster-store,omitempty"`
// ClusterOpts is used to pass options to the discovery package for tuning libkv settings, such // ClusterOpts is used to pass options to the discovery package for tuning libkv settings, such
// as TLS configuration settings. // as TLS configuration settings.
ClusterOpts map[string]string ClusterOpts map[string]string `json:"cluster-store-opts,omitempty"`
// ClusterAdvertise is the network endpoint that the Engine advertises for the purpose of node // ClusterAdvertise is the network endpoint that the Engine advertises for the purpose of node
// discovery. This should be a 'host:port' combination on which that daemon instance is // discovery. This should be a 'host:port' combination on which that daemon instance is
// reachable by other hosts. // reachable by other hosts.
ClusterAdvertise string ClusterAdvertise string `json:"cluster-advertise,omitempty"`
Debug bool `json:"debug,omitempty"`
Hosts []string `json:"hosts,omitempty"`
LogLevel string `json:"log-level,omitempty"`
TLS bool `json:"tls,omitempty"`
TLSVerify bool `json:"tls-verify,omitempty"`
TLSOptions CommonTLSOptions `json:"tls-opts,omitempty"`
reloadLock sync.Mutex
} }
// InstallCommonFlags adds command-line options to the top-level flag parser for // InstallCommonFlags adds command-line options to the top-level flag parser for
@ -54,9 +91,9 @@ type CommonConfig struct {
// Subsequent calls to `flag.Parse` will populate config with values parsed // Subsequent calls to `flag.Parse` will populate config with values parsed
// from the command-line. // from the command-line.
func (config *Config) InstallCommonFlags(cmd *flag.FlagSet, usageFn func(string) string) { func (config *Config) InstallCommonFlags(cmd *flag.FlagSet, usageFn func(string) string) {
cmd.Var(opts.NewListOptsRef(&config.GraphOptions, nil), []string{"-storage-opt"}, usageFn("Set storage driver options")) cmd.Var(opts.NewNamedListOptsRef("storage-opts", &config.GraphOptions, nil), []string{"-storage-opt"}, usageFn("Set storage driver options"))
cmd.Var(opts.NewListOptsRef(&config.AuthorizationPlugins, nil), []string{"-authorization-plugin"}, usageFn("List authorization plugins in order from first evaluator to last")) cmd.Var(opts.NewNamedListOptsRef("authorization-plugins", &config.AuthorizationPlugins, nil), []string{"-authorization-plugin"}, usageFn("List authorization plugins in order from first evaluator to last"))
cmd.Var(opts.NewListOptsRef(&config.ExecOptions, nil), []string{"-exec-opt"}, usageFn("Set exec driver options")) cmd.Var(opts.NewNamedListOptsRef("exec-opts", &config.ExecOptions, nil), []string{"-exec-opt"}, usageFn("Set exec driver options"))
cmd.StringVar(&config.Pidfile, []string{"p", "-pidfile"}, defaultPidFile, usageFn("Path to use for daemon PID file")) cmd.StringVar(&config.Pidfile, []string{"p", "-pidfile"}, defaultPidFile, usageFn("Path to use for daemon PID file"))
cmd.StringVar(&config.Root, []string{"g", "-graph"}, defaultGraph, usageFn("Root of the Docker runtime")) cmd.StringVar(&config.Root, []string{"g", "-graph"}, defaultGraph, usageFn("Root of the Docker runtime"))
cmd.StringVar(&config.ExecRoot, []string{"-exec-root"}, "/var/run/docker", usageFn("Root of the Docker execdriver")) 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)
cmd.IntVar(&config.Mtu, []string{"#mtu", "-mtu"}, 0, usageFn("Set the containers network MTU")) cmd.IntVar(&config.Mtu, []string{"#mtu", "-mtu"}, 0, usageFn("Set the containers network MTU"))
// FIXME: why the inconsistency between "hosts" and "sockets"? // FIXME: why the inconsistency between "hosts" and "sockets"?
cmd.Var(opts.NewListOptsRef(&config.DNS, opts.ValidateIPAddress), []string{"#dns", "-dns"}, usageFn("DNS server to use")) cmd.Var(opts.NewListOptsRef(&config.DNS, opts.ValidateIPAddress), []string{"#dns", "-dns"}, usageFn("DNS server to use"))
cmd.Var(opts.NewListOptsRef(&config.DNSOptions, nil), []string{"-dns-opt"}, usageFn("DNS options to use")) cmd.Var(opts.NewNamedListOptsRef("dns-opts", &config.DNSOptions, nil), []string{"-dns-opt"}, usageFn("DNS options to use"))
cmd.Var(opts.NewListOptsRef(&config.DNSSearch, opts.ValidateDNSSearch), []string{"-dns-search"}, usageFn("DNS search domains to use")) cmd.Var(opts.NewListOptsRef(&config.DNSSearch, opts.ValidateDNSSearch), []string{"-dns-search"}, usageFn("DNS search domains to use"))
cmd.Var(opts.NewListOptsRef(&config.Labels, opts.ValidateLabel), []string{"-label"}, usageFn("Set key=value labels to the daemon")) cmd.Var(opts.NewNamedListOptsRef("labels", &config.Labels, opts.ValidateLabel), []string{"-label"}, usageFn("Set key=value labels to the daemon"))
cmd.StringVar(&config.LogConfig.Type, []string{"-log-driver"}, "json-file", usageFn("Default driver for container logs")) cmd.StringVar(&config.LogConfig.Type, []string{"-log-driver"}, "json-file", usageFn("Default driver for container logs"))
cmd.Var(opts.NewMapOpts(config.LogConfig.Config, nil), []string{"-log-opt"}, usageFn("Set log driver options")) cmd.Var(opts.NewNamedMapOpts("log-opts", config.LogConfig.Config, nil), []string{"-log-opt"}, usageFn("Set log driver options"))
cmd.StringVar(&config.ClusterAdvertise, []string{"-cluster-advertise"}, "", usageFn("Address or interface name to advertise")) cmd.StringVar(&config.ClusterAdvertise, []string{"-cluster-advertise"}, "", usageFn("Address or interface name to advertise"))
cmd.StringVar(&config.ClusterStore, []string{"-cluster-store"}, "", usageFn("Set the cluster store")) cmd.StringVar(&config.ClusterStore, []string{"-cluster-store"}, "", usageFn("Set the cluster store"))
cmd.Var(opts.NewMapOpts(config.ClusterOpts, nil), []string{"-cluster-store-opt"}, usageFn("Set cluster store options")) cmd.Var(opts.NewNamedMapOpts("cluster-store-opts", config.ClusterOpts, nil), []string{"-cluster-store-opt"}, usageFn("Set cluster store options"))
}
func parseClusterAdvertiseSettings(clusterStore, clusterAdvertise string) (string, error) {
if clusterAdvertise == "" {
return "", errDiscoveryDisabled
}
if clusterStore == "" {
return "", fmt.Errorf("invalid cluster configuration. --cluster-advertise must be accompanied by --cluster-store configuration")
}
advertise, err := discovery.ParseAdvertise(clusterAdvertise)
if err != nil {
return "", fmt.Errorf("discovery advertise parsing failed (%v)", err)
}
return advertise, nil
}
// ReloadConfiguration reads the configuration in the host and reloads the daemon and server.
func ReloadConfiguration(configFile string, flags *flag.FlagSet, reload func(*Config)) {
logrus.Infof("Got signal to reload configuration, reloading from: %s", configFile)
newConfig, err := getConflictFreeConfiguration(configFile, flags)
if err != nil {
logrus.Error(err)
} else {
reload(newConfig)
}
}
// MergeDaemonConfigurations reads a configuration file,
// loads the file configuration in an isolated structure,
// and merges the configuration provided from flags on top
// if there are no conflicts.
func MergeDaemonConfigurations(flagsConfig *Config, flags *flag.FlagSet, configFile string) (*Config, error) {
fileConfig, err := getConflictFreeConfiguration(configFile, flags)
if err != nil {
return nil, err
}
// merge flags configuration on top of the file configuration
if err := mergo.Merge(fileConfig, flagsConfig); err != nil {
return nil, err
}
return fileConfig, nil
}
// getConflictFreeConfiguration loads the configuration from a JSON file.
// It compares that configuration with the one provided by the flags,
// and returns an error if there are conflicts.
func getConflictFreeConfiguration(configFile string, flags *flag.FlagSet) (*Config, error) {
b, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, err
}
var reader io.Reader
if flags != nil {
var jsonConfig map[string]interface{}
reader = bytes.NewReader(b)
if err := json.NewDecoder(reader).Decode(&jsonConfig); err != nil {
return nil, err
}
if err := findConfigurationConflicts(jsonConfig, flags); err != nil {
return nil, err
}
}
var config Config
reader = bytes.NewReader(b)
err = json.NewDecoder(reader).Decode(&config)
return &config, err
}
// findConfigurationConflicts iterates over the provided flags searching for
// duplicated configurations. It returns an error with all the conflicts if
// it finds any.
func findConfigurationConflicts(config map[string]interface{}, flags *flag.FlagSet) error {
var conflicts []string
flatten := make(map[string]interface{})
for k, v := range config {
if m, ok := v.(map[string]interface{}); ok {
for km, vm := range m {
flatten[km] = vm
}
} else {
flatten[k] = v
}
}
printConflict := func(name string, flagValue, fileValue interface{}) string {
return fmt.Sprintf("%s: (from flag: %v, from file: %v)", name, flagValue, fileValue)
}
collectConflicts := func(f *flag.Flag) {
// search option name in the json configuration payload if the value is a named option
if namedOption, ok := f.Value.(opts.NamedOption); ok {
if optsValue, ok := flatten[namedOption.Name()]; ok {
conflicts = append(conflicts, printConflict(namedOption.Name(), f.Value.String(), optsValue))
}
} else {
// search flag name in the json configuration payload without trailing dashes
for _, name := range f.Names {
name = strings.TrimLeft(name, "-")
if value, ok := flatten[name]; ok {
conflicts = append(conflicts, printConflict(name, f.Value.String(), value))
break
}
}
}
}
flags.Visit(collectConflicts)
if len(conflicts) > 0 {
return fmt.Errorf("the following directives are specified both as a flag and in the configuration file: %s", strings.Join(conflicts, ", "))
}
return nil
} }

177
daemon/config_test.go Normal file
View File

@ -0,0 +1,177 @@
package daemon
import (
"io/ioutil"
"os"
"strings"
"testing"
"github.com/docker/docker/opts"
"github.com/docker/docker/pkg/mflag"
)
func TestDaemonConfigurationMerge(t *testing.T) {
f, err := ioutil.TempFile("", "docker-config-")
if err != nil {
t.Fatal(err)
}
configFile := f.Name()
f.Write([]byte(`{"debug": true}`))
f.Close()
c := &Config{
CommonConfig: CommonConfig{
AutoRestart: true,
LogConfig: LogConfig{
Type: "syslog",
Config: map[string]string{"tag": "test"},
},
},
}
cc, err := MergeDaemonConfigurations(c, nil, configFile)
if err != nil {
t.Fatal(err)
}
if !cc.Debug {
t.Fatalf("expected %v, got %v\n", true, cc.Debug)
}
if !cc.AutoRestart {
t.Fatalf("expected %v, got %v\n", true, cc.AutoRestart)
}
if cc.LogConfig.Type != "syslog" {
t.Fatalf("expected syslog config, got %q\n", cc.LogConfig)
}
}
func TestDaemonConfigurationNotFound(t *testing.T) {
_, err := MergeDaemonConfigurations(&Config{}, nil, "/tmp/foo-bar-baz-docker")
if err == nil || !os.IsNotExist(err) {
t.Fatalf("expected does not exist error, got %v", err)
}
}
func TestDaemonBrokenConfiguration(t *testing.T) {
f, err := ioutil.TempFile("", "docker-config-")
if err != nil {
t.Fatal(err)
}
configFile := f.Name()
f.Write([]byte(`{"Debug": tru`))
f.Close()
_, err = MergeDaemonConfigurations(&Config{}, nil, configFile)
if err == nil {
t.Fatalf("expected error, got %v", err)
}
}
func TestParseClusterAdvertiseSettings(t *testing.T) {
_, err := parseClusterAdvertiseSettings("something", "")
if err != errDiscoveryDisabled {
t.Fatalf("expected discovery disabled error, got %v\n", err)
}
_, err = parseClusterAdvertiseSettings("", "something")
if err == nil {
t.Fatalf("expected discovery store error, got %v\n", err)
}
_, err = parseClusterAdvertiseSettings("etcd", "127.0.0.1:8080")
if err != nil {
t.Fatal(err)
}
}
func TestFindConfigurationConflicts(t *testing.T) {
config := map[string]interface{}{"authorization-plugins": "foobar"}
flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
err := findConfigurationConflicts(config, flags)
if err != nil {
t.Fatal(err)
}
flags.String([]string{"authorization-plugins"}, "", "")
if err := flags.Set("authorization-plugins", "asdf"); err != nil {
t.Fatal(err)
}
err = findConfigurationConflicts(config, flags)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "authorization-plugins") {
t.Fatalf("expected authorization-plugins conflict, got %v", err)
}
}
func TestFindConfigurationConflictsWithNamedOptions(t *testing.T) {
config := map[string]interface{}{"hosts": []string{"qwer"}}
flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
var hosts []string
flags.Var(opts.NewNamedListOptsRef("hosts", &hosts, opts.ValidateHost), []string{"H", "-host"}, "Daemon socket(s) to connect to")
if err := flags.Set("-host", "tcp://127.0.0.1:4444"); err != nil {
t.Fatal(err)
}
if err := flags.Set("H", "unix:///var/run/docker.sock"); err != nil {
t.Fatal(err)
}
err := findConfigurationConflicts(config, flags)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "hosts") {
t.Fatalf("expected hosts conflict, got %v", err)
}
}
func TestDaemonConfigurationMergeConflicts(t *testing.T) {
f, err := ioutil.TempFile("", "docker-config-")
if err != nil {
t.Fatal(err)
}
configFile := f.Name()
f.Write([]byte(`{"debug": true}`))
f.Close()
flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
flags.Bool([]string{"debug"}, false, "")
flags.Set("debug", "false")
_, err = MergeDaemonConfigurations(&Config{}, flags, configFile)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "debug") {
t.Fatalf("expected debug conflict, got %v", err)
}
}
func TestDaemonConfigurationMergeConflictsWithInnerStructs(t *testing.T) {
f, err := ioutil.TempFile("", "docker-config-")
if err != nil {
t.Fatal(err)
}
configFile := f.Name()
f.Write([]byte(`{"tlscacert": "/etc/certificates/ca.pem"}`))
f.Close()
flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
flags.String([]string{"tlscacert"}, "", "")
flags.Set("tlscacert", "~/.docker/ca.pem")
_, err = MergeDaemonConfigurations(&Config{}, flags, configFile)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "tlscacert") {
t.Fatalf("expected tlscacert conflict, got %v", err)
}
}

View File

@ -18,18 +18,20 @@ var (
) )
// Config defines the configuration of a docker daemon. // Config defines the configuration of a docker daemon.
// It includes json tags to deserialize configuration from a file
// using the same names that the flags in the command line uses.
type Config struct { type Config struct {
CommonConfig CommonConfig
// Fields below here are platform specific. // Fields below here are platform specific.
CorsHeaders string CorsHeaders string `json:"api-cors-headers,omitempty"`
EnableCors bool EnableCors bool `json:"api-enable-cors,omitempty"`
EnableSelinuxSupport bool EnableSelinuxSupport bool `json:"selinux-enabled,omitempty"`
RemappedRoot string RemappedRoot string `json:"userns-remap,omitempty"`
SocketGroup string SocketGroup string `json:"group,omitempty"`
CgroupParent string CgroupParent string `json:"cgroup-parent,omitempty"`
Ulimits map[string]*units.Ulimit Ulimits map[string]*units.Ulimit `json:"default-ulimits,omitempty"`
} }
// bridgeConfig stores all the bridge driver specific // bridgeConfig stores all the bridge driver specific

View File

@ -46,7 +46,6 @@ import (
"github.com/docker/docker/layer" "github.com/docker/docker/layer"
"github.com/docker/docker/migrate/v1" "github.com/docker/docker/migrate/v1"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/discovery"
"github.com/docker/docker/pkg/fileutils" "github.com/docker/docker/pkg/fileutils"
"github.com/docker/docker/pkg/graphdb" "github.com/docker/docker/pkg/graphdb"
"github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/idtools"
@ -155,7 +154,7 @@ type Daemon struct {
EventsService *events.Events EventsService *events.Events
netController libnetwork.NetworkController netController libnetwork.NetworkController
volumes *store.VolumeStore volumes *store.VolumeStore
discoveryWatcher discovery.Watcher discoveryWatcher discoveryReloader
root string root string
seccompEnabled bool seccompEnabled bool
shutdown bool shutdown bool
@ -292,7 +291,7 @@ func (daemon *Daemon) Register(container *container.Container) error {
func (daemon *Daemon) restore() error { func (daemon *Daemon) restore() error {
var ( var (
debug = os.Getenv("DEBUG") != "" debug = utils.IsDebugEnabled()
currentDriver = daemon.GraphDriverName() currentDriver = daemon.GraphDriverName()
containers = make(map[string]*container.Container) containers = make(map[string]*container.Container)
) )
@ -772,19 +771,8 @@ func NewDaemon(config *Config, registryService *registry.Service) (daemon *Daemo
// Discovery is only enabled when the daemon is launched with an address to advertise. When // Discovery is only enabled when the daemon is launched with an address to advertise. When
// initialized, the daemon is registered and we can store the discovery backend as its read-only // initialized, the daemon is registered and we can store the discovery backend as its read-only
// DiscoveryWatcher version. if err := d.initDiscovery(config); err != nil {
if config.ClusterStore != "" && config.ClusterAdvertise != "" { return nil, err
advertise, err := discovery.ParseAdvertise(config.ClusterStore, config.ClusterAdvertise)
if err != nil {
return nil, fmt.Errorf("discovery advertise parsing failed (%v)", err)
}
config.ClusterAdvertise = advertise
d.discoveryWatcher, err = initDiscovery(config.ClusterStore, config.ClusterAdvertise, config.ClusterOpts)
if err != nil {
return nil, fmt.Errorf("discovery initialization failed (%v)", err)
}
} else if config.ClusterAdvertise != "" {
return nil, fmt.Errorf("invalid cluster configuration. --cluster-advertise must be accompanied by --cluster-store configuration")
} }
d.netController, err = d.initNetworkController(config) d.netController, err = d.initNetworkController(config)
@ -815,7 +803,10 @@ func NewDaemon(config *Config, registryService *registry.Service) (daemon *Daemo
d.configStore = config d.configStore = config
d.execDriver = ed d.execDriver = ed
d.statsCollector = d.newStatsCollector(1 * time.Second) d.statsCollector = d.newStatsCollector(1 * time.Second)
d.defaultLogConfig = config.LogConfig d.defaultLogConfig = containertypes.LogConfig{
Type: config.LogConfig.Type,
Config: config.LogConfig.Config,
}
d.RegistryService = registryService d.RegistryService = registryService
d.EventsService = eventsService d.EventsService = eventsService
d.volumes = volStore d.volumes = volStore
@ -1521,6 +1512,76 @@ func (daemon *Daemon) newBaseContainer(id string) *container.Container {
return container.NewBaseContainer(id, daemon.containerRoot(id)) return container.NewBaseContainer(id, daemon.containerRoot(id))
} }
// initDiscovery initializes the discovery watcher for this daemon.
func (daemon *Daemon) initDiscovery(config *Config) error {
advertise, err := parseClusterAdvertiseSettings(config.ClusterStore, config.ClusterAdvertise)
if err != nil {
if err == errDiscoveryDisabled {
return nil
}
return err
}
config.ClusterAdvertise = advertise
discoveryWatcher, err := initDiscovery(config.ClusterStore, config.ClusterAdvertise, config.ClusterOpts)
if err != nil {
return fmt.Errorf("discovery initialization failed (%v)", err)
}
daemon.discoveryWatcher = discoveryWatcher
return nil
}
// Reload reads configuration changes and modifies the
// daemon according to those changes.
// This are the settings that Reload changes:
// - Daemon labels.
// - Cluster discovery (reconfigure and restart).
func (daemon *Daemon) Reload(config *Config) error {
daemon.configStore.reloadLock.Lock()
defer daemon.configStore.reloadLock.Unlock()
daemon.configStore.Labels = config.Labels
return daemon.reloadClusterDiscovery(config)
}
func (daemon *Daemon) reloadClusterDiscovery(config *Config) error {
newAdvertise, err := parseClusterAdvertiseSettings(config.ClusterStore, config.ClusterAdvertise)
if err != nil && err != errDiscoveryDisabled {
return err
}
// check discovery modifications
if !modifiedDiscoverySettings(daemon.configStore, newAdvertise, config.ClusterStore, config.ClusterOpts) {
return nil
}
// enable discovery for the first time if it was not previously enabled
if daemon.discoveryWatcher == nil {
discoveryWatcher, err := initDiscovery(config.ClusterStore, newAdvertise, config.ClusterOpts)
if err != nil {
return fmt.Errorf("discovery initialization failed (%v)", err)
}
daemon.discoveryWatcher = discoveryWatcher
} else {
if err == errDiscoveryDisabled {
// disable discovery if it was previously enabled and it's disabled now
daemon.discoveryWatcher.Stop()
} else {
// reload discovery
if err = daemon.discoveryWatcher.Reload(config.ClusterStore, newAdvertise, config.ClusterOpts); err != nil {
return err
}
}
}
daemon.configStore.ClusterStore = config.ClusterStore
daemon.configStore.ClusterOpts = config.ClusterOpts
daemon.configStore.ClusterAdvertise = newAdvertise
return nil
}
func convertLnNetworkStats(name string, stats *lntypes.InterfaceStatistics) *libcontainer.NetworkInterface { func convertLnNetworkStats(name string, stats *lntypes.InterfaceStatistics) *libcontainer.NetworkInterface {
n := &libcontainer.NetworkInterface{Name: name} n := &libcontainer.NetworkInterface{Name: name}
n.RxBytes = stats.RxBytes n.RxBytes = stats.RxBytes

View File

@ -4,9 +4,13 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"testing" "testing"
"time"
"github.com/docker/docker/container" "github.com/docker/docker/container"
"github.com/docker/docker/pkg/discovery"
_ "github.com/docker/docker/pkg/discovery/memory"
"github.com/docker/docker/pkg/registrar" "github.com/docker/docker/pkg/registrar"
"github.com/docker/docker/pkg/truncindex" "github.com/docker/docker/pkg/truncindex"
"github.com/docker/docker/volume" "github.com/docker/docker/volume"
@ -371,3 +375,118 @@ func TestMerge(t *testing.T) {
} }
} }
} }
func TestDaemonReloadLabels(t *testing.T) {
daemon := &Daemon{}
daemon.configStore = &Config{
CommonConfig: CommonConfig{
Labels: []string{"foo:bar"},
},
}
newConfig := &Config{
CommonConfig: CommonConfig{
Labels: []string{"foo:baz"},
},
}
daemon.Reload(newConfig)
label := daemon.configStore.Labels[0]
if label != "foo:baz" {
t.Fatalf("Expected daemon label `foo:baz`, got %s", label)
}
}
func TestDaemonDiscoveryReload(t *testing.T) {
daemon := &Daemon{}
daemon.configStore = &Config{
CommonConfig: CommonConfig{
ClusterStore: "memory://127.0.0.1",
ClusterAdvertise: "127.0.0.1:3333",
},
}
if err := daemon.initDiscovery(daemon.configStore); err != nil {
t.Fatal(err)
}
expected := discovery.Entries{
&discovery.Entry{Host: "127.0.0.1", Port: "3333"},
}
stopCh := make(chan struct{})
defer close(stopCh)
ch, errCh := daemon.discoveryWatcher.Watch(stopCh)
select {
case <-time.After(1 * time.Second):
t.Fatal("failed to get discovery advertisements in time")
case e := <-ch:
if !reflect.DeepEqual(e, expected) {
t.Fatalf("expected %v, got %v\n", expected, e)
}
case e := <-errCh:
t.Fatal(e)
}
newConfig := &Config{
CommonConfig: CommonConfig{
ClusterStore: "memory://127.0.0.1:2222",
ClusterAdvertise: "127.0.0.1:5555",
},
}
expected = discovery.Entries{
&discovery.Entry{Host: "127.0.0.1", Port: "5555"},
}
if err := daemon.Reload(newConfig); err != nil {
t.Fatal(err)
}
ch, errCh = daemon.discoveryWatcher.Watch(stopCh)
select {
case <-time.After(1 * time.Second):
t.Fatal("failed to get discovery advertisements in time")
case e := <-ch:
if !reflect.DeepEqual(e, expected) {
t.Fatalf("expected %v, got %v\n", expected, e)
}
case e := <-errCh:
t.Fatal(e)
}
}
func TestDaemonDiscoveryReloadFromEmptyDiscovery(t *testing.T) {
daemon := &Daemon{}
daemon.configStore = &Config{}
newConfig := &Config{
CommonConfig: CommonConfig{
ClusterStore: "memory://127.0.0.1:2222",
ClusterAdvertise: "127.0.0.1:5555",
},
}
expected := discovery.Entries{
&discovery.Entry{Host: "127.0.0.1", Port: "5555"},
}
if err := daemon.Reload(newConfig); err != nil {
t.Fatal(err)
}
stopCh := make(chan struct{})
defer close(stopCh)
ch, errCh := daemon.discoveryWatcher.Watch(stopCh)
select {
case <-time.After(1 * time.Second):
t.Fatal("failed to get discovery advertisements in time")
case e := <-ch:
if !reflect.DeepEqual(e, expected) {
t.Fatalf("expected %v, got %v\n", expected, e)
}
case e := <-errCh:
t.Fatal(e)
}
}

View File

@ -1,7 +1,9 @@
package daemon package daemon
import ( import (
"errors"
"fmt" "fmt"
"reflect"
"strconv" "strconv"
"time" "time"
@ -19,6 +21,24 @@ const (
defaultDiscoveryTTLFactor = 3 defaultDiscoveryTTLFactor = 3
) )
var errDiscoveryDisabled = errors.New("discovery is disabled")
type discoveryReloader interface {
discovery.Watcher
Stop()
Reload(backend, address string, clusterOpts map[string]string) error
}
type daemonDiscoveryReloader struct {
backend discovery.Backend
ticker *time.Ticker
term chan bool
}
func (d *daemonDiscoveryReloader) Watch(stopCh <-chan struct{}) (<-chan discovery.Entries, <-chan error) {
return d.backend.Watch(stopCh)
}
func discoveryOpts(clusterOpts map[string]string) (time.Duration, time.Duration, error) { func discoveryOpts(clusterOpts map[string]string) (time.Duration, time.Duration, error) {
var ( var (
heartbeat = defaultDiscoveryHeartbeat heartbeat = defaultDiscoveryHeartbeat
@ -57,36 +77,94 @@ func discoveryOpts(clusterOpts map[string]string) (time.Duration, time.Duration,
// initDiscovery initialized the nodes discovery subsystem by connecting to the specified backend // initDiscovery initialized the nodes discovery subsystem by connecting to the specified backend
// and start a registration loop to advertise the current node under the specified address. // and start a registration loop to advertise the current node under the specified address.
func initDiscovery(backend, address string, clusterOpts map[string]string) (discovery.Backend, error) { func initDiscovery(backendAddress, advertiseAddress string, clusterOpts map[string]string) (discoveryReloader, error) {
heartbeat, backend, err := parseDiscoveryOptions(backendAddress, clusterOpts)
heartbeat, ttl, err := discoveryOpts(clusterOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
discoveryBackend, err := discovery.New(backend, heartbeat, ttl, clusterOpts) reloader := &daemonDiscoveryReloader{
if err != nil { backend: backend,
return nil, err ticker: time.NewTicker(heartbeat),
term: make(chan bool),
} }
// We call Register() on the discovery backend in a loop for the whole lifetime of the daemon, // We call Register() on the discovery backend in a loop for the whole lifetime of the daemon,
// but we never actually Watch() for nodes appearing and disappearing for the moment. // but we never actually Watch() for nodes appearing and disappearing for the moment.
go registrationLoop(discoveryBackend, address, heartbeat) reloader.advertise(advertiseAddress)
return discoveryBackend, nil return reloader, nil
} }
func registerAddr(backend discovery.Backend, addr string) { func (d *daemonDiscoveryReloader) advertise(address string) {
if err := backend.Register(addr); err != nil { d.registerAddr(address)
go d.advertiseHeartbeat(address)
}
func (d *daemonDiscoveryReloader) registerAddr(addr string) {
if err := d.backend.Register(addr); err != nil {
log.Warnf("Registering as %q in discovery failed: %v", addr, err) log.Warnf("Registering as %q in discovery failed: %v", addr, err)
} }
} }
// registrationLoop registers the current node against the discovery backend using the specified // advertiseHeartbeat registers the current node against the discovery backend using the specified
// address. The function never returns, as registration against the backend comes with a TTL and // address. The function never returns, as registration against the backend comes with a TTL and
// requires regular heartbeats. // requires regular heartbeats.
func registrationLoop(discoveryBackend discovery.Backend, address string, heartbeat time.Duration) { func (d *daemonDiscoveryReloader) advertiseHeartbeat(address string) {
registerAddr(discoveryBackend, address) for {
for range time.Tick(heartbeat) { select {
registerAddr(discoveryBackend, address) case <-d.ticker.C:
d.registerAddr(address)
case <-d.term:
return
}
} }
} }
// Reload makes the watcher to stop advertising and reconfigures it to advertise in a new address.
func (d *daemonDiscoveryReloader) Reload(backendAddress, advertiseAddress string, clusterOpts map[string]string) error {
d.Stop()
heartbeat, backend, err := parseDiscoveryOptions(backendAddress, clusterOpts)
if err != nil {
return err
}
d.backend = backend
d.ticker = time.NewTicker(heartbeat)
d.advertise(advertiseAddress)
return nil
}
// Stop terminates the discovery advertising.
func (d *daemonDiscoveryReloader) Stop() {
d.ticker.Stop()
d.term <- true
}
func parseDiscoveryOptions(backendAddress string, clusterOpts map[string]string) (time.Duration, discovery.Backend, error) {
heartbeat, ttl, err := discoveryOpts(clusterOpts)
if err != nil {
return 0, nil, err
}
backend, err := discovery.New(backendAddress, heartbeat, ttl, clusterOpts)
if err != nil {
return 0, nil, err
}
return heartbeat, backend, nil
}
// modifiedDiscoverySettings returns whether the discovery configuration has been modified or not.
func modifiedDiscoverySettings(config *Config, backendType, advertise string, clusterOpts map[string]string) bool {
if config.ClusterStore != backendType || config.ClusterAdvertise != advertise {
return true
}
if (config.ClusterOpts == nil && clusterOpts == nil) ||
(config.ClusterOpts == nil && len(clusterOpts) == 0) ||
(len(config.ClusterOpts) == 0 && clusterOpts == nil) {
return false
}
return !reflect.DeepEqual(config.ClusterOpts, clusterOpts)
}

View File

@ -89,3 +89,64 @@ func TestDiscoveryOpts(t *testing.T) {
t.Fatalf("TTL - Expected : %v, Actual : %v", expected, ttl) t.Fatalf("TTL - Expected : %v, Actual : %v", expected, ttl)
} }
} }
func TestModifiedDiscoverySettings(t *testing.T) {
cases := []struct {
current *Config
modified *Config
expected bool
}{
{
current: discoveryConfig("foo", "bar", map[string]string{}),
modified: discoveryConfig("foo", "bar", map[string]string{}),
expected: false,
},
{
current: discoveryConfig("foo", "bar", map[string]string{"foo": "bar"}),
modified: discoveryConfig("foo", "bar", map[string]string{"foo": "bar"}),
expected: false,
},
{
current: discoveryConfig("foo", "bar", map[string]string{}),
modified: discoveryConfig("foo", "bar", nil),
expected: false,
},
{
current: discoveryConfig("foo", "bar", nil),
modified: discoveryConfig("foo", "bar", map[string]string{}),
expected: false,
},
{
current: discoveryConfig("foo", "bar", nil),
modified: discoveryConfig("baz", "bar", nil),
expected: true,
},
{
current: discoveryConfig("foo", "bar", nil),
modified: discoveryConfig("foo", "baz", nil),
expected: true,
},
{
current: discoveryConfig("foo", "bar", nil),
modified: discoveryConfig("foo", "bar", map[string]string{"foo": "bar"}),
expected: true,
},
}
for _, c := range cases {
got := modifiedDiscoverySettings(c.current, c.modified.ClusterStore, c.modified.ClusterAdvertise, c.modified.ClusterOpts)
if c.expected != got {
t.Fatalf("expected %v, got %v: current config %q, new config %q", c.expected, got, c.current, c.modified)
}
}
}
func discoveryConfig(backendAddr, advertiseAddr string, opts map[string]string) *Config {
return &Config{
CommonConfig: CommonConfig{
ClusterStore: backendAddr,
ClusterAdvertise: advertiseAddr,
ClusterOpts: opts,
},
}
}

View File

@ -79,7 +79,7 @@ func (daemon *Daemon) SystemInfo() (*types.Info, error) {
IPv4Forwarding: !sysInfo.IPv4ForwardingDisabled, IPv4Forwarding: !sysInfo.IPv4ForwardingDisabled,
BridgeNfIptables: !sysInfo.BridgeNfCallIptablesDisabled, BridgeNfIptables: !sysInfo.BridgeNfCallIptablesDisabled,
BridgeNfIP6tables: !sysInfo.BridgeNfCallIP6tablesDisabled, BridgeNfIP6tables: !sysInfo.BridgeNfCallIP6tablesDisabled,
Debug: os.Getenv("DEBUG") != "", Debug: utils.IsDebugEnabled(),
NFd: fileutils.GetTotalUsedFds(), NFd: fileutils.GetTotalUsedFds(),
NGoroutines: runtime.NumGoroutine(), NGoroutines: runtime.NumGoroutine(),
SystemTime: time.Now().Format(time.RFC3339Nano), SystemTime: time.Now().Format(time.RFC3339Nano),

View File

@ -21,7 +21,6 @@ const (
) )
var ( var (
daemonFlags *flag.FlagSet
commonFlags = &cli.CommonFlags{FlagSet: new(flag.FlagSet)} commonFlags = &cli.CommonFlags{FlagSet: new(flag.FlagSet)}
dockerCertPath = os.Getenv("DOCKER_CERT_PATH") dockerCertPath = os.Getenv("DOCKER_CERT_PATH")
@ -50,7 +49,7 @@ func init() {
cmd.StringVar(&tlsOptions.CertFile, []string{"-tlscert"}, filepath.Join(dockerCertPath, defaultCertFile), "Path to TLS certificate file") cmd.StringVar(&tlsOptions.CertFile, []string{"-tlscert"}, filepath.Join(dockerCertPath, defaultCertFile), "Path to TLS certificate file")
cmd.StringVar(&tlsOptions.KeyFile, []string{"-tlskey"}, filepath.Join(dockerCertPath, defaultKeyFile), "Path to TLS key file") cmd.StringVar(&tlsOptions.KeyFile, []string{"-tlskey"}, filepath.Join(dockerCertPath, defaultKeyFile), "Path to TLS key file")
cmd.Var(opts.NewListOptsRef(&commonFlags.Hosts, opts.ValidateHost), []string{"H", "-host"}, "Daemon socket(s) to connect to") cmd.Var(opts.NewNamedListOptsRef("hosts", &commonFlags.Hosts, opts.ValidateHost), []string{"H", "-host"}, "Daemon socket(s) to connect to")
} }
func postParseCommon() { func postParseCommon() {
@ -67,11 +66,6 @@ func postParseCommon() {
logrus.SetLevel(logrus.InfoLevel) logrus.SetLevel(logrus.InfoLevel)
} }
if commonFlags.Debug {
os.Setenv("DEBUG", "1")
logrus.SetLevel(logrus.DebugLevel)
}
// Regardless of whether the user sets it to true or false, if they // Regardless of whether the user sets it to true or false, if they
// specify --tlsverify at all then we need to turn on tls // specify --tlsverify at all then we need to turn on tls
// TLSVerify can be true even if not set due to DOCKER_TLS_VERIFY env var, so we need to check that here as well // TLSVerify can be true even if not set due to DOCKER_TLS_VERIFY env var, so we need to check that here as well

View File

@ -30,23 +30,34 @@ import (
"github.com/docker/go-connections/tlsconfig" "github.com/docker/go-connections/tlsconfig"
) )
const daemonUsage = " docker daemon [ --help | ... ]\n" const (
daemonUsage = " docker daemon [ --help | ... ]\n"
daemonConfigFileFlag = "-config-file"
)
var ( var (
daemonCli cli.Handler = NewDaemonCli() daemonCli cli.Handler = NewDaemonCli()
) )
// DaemonCli represents the daemon CLI.
type DaemonCli struct {
*daemon.Config
registryOptions *registry.Options
flags *flag.FlagSet
}
func presentInHelp(usage string) string { return usage } func presentInHelp(usage string) string { return usage }
func absentFromHelp(string) string { return "" } func absentFromHelp(string) string { return "" }
// NewDaemonCli returns a pre-configured daemon CLI // NewDaemonCli returns a pre-configured daemon CLI
func NewDaemonCli() *DaemonCli { func NewDaemonCli() *DaemonCli {
daemonFlags = cli.Subcmd("daemon", nil, "Enable daemon mode", true) daemonFlags := cli.Subcmd("daemon", nil, "Enable daemon mode", true)
// TODO(tiborvass): remove InstallFlags? // TODO(tiborvass): remove InstallFlags?
daemonConfig := new(daemon.Config) daemonConfig := new(daemon.Config)
daemonConfig.LogConfig.Config = make(map[string]string) daemonConfig.LogConfig.Config = make(map[string]string)
daemonConfig.ClusterOpts = make(map[string]string) daemonConfig.ClusterOpts = make(map[string]string)
daemonConfig.InstallFlags(daemonFlags, presentInHelp) daemonConfig.InstallFlags(daemonFlags, presentInHelp)
daemonConfig.InstallFlags(flag.CommandLine, absentFromHelp) daemonConfig.InstallFlags(flag.CommandLine, absentFromHelp)
registryOptions := new(registry.Options) registryOptions := new(registry.Options)
@ -57,6 +68,7 @@ func NewDaemonCli() *DaemonCli {
return &DaemonCli{ return &DaemonCli{
Config: daemonConfig, Config: daemonConfig,
registryOptions: registryOptions, registryOptions: registryOptions,
flags: daemonFlags,
} }
} }
@ -101,12 +113,6 @@ func migrateKey() (err error) {
return nil return nil
} }
// DaemonCli represents the daemon CLI.
type DaemonCli struct {
*daemon.Config
registryOptions *registry.Options
}
func getGlobalFlag() (globalFlag *flag.Flag) { func getGlobalFlag() (globalFlag *flag.Flag) {
defer func() { defer func() {
if x := recover(); x != nil { if x := recover(); x != nil {
@ -136,15 +142,27 @@ func (cli *DaemonCli) CmdDaemon(args ...string) error {
os.Exit(1) os.Exit(1)
} else { } else {
// allow new form `docker daemon -D` // allow new form `docker daemon -D`
flag.Merge(daemonFlags, commonFlags.FlagSet) flag.Merge(cli.flags, commonFlags.FlagSet)
} }
daemonFlags.ParseFlags(args, true) configFile := cli.flags.String([]string{daemonConfigFileFlag}, defaultDaemonConfigFile, "Daemon configuration file")
cli.flags.ParseFlags(args, true)
commonFlags.PostParse() commonFlags.PostParse()
if commonFlags.TrustKey == "" { if commonFlags.TrustKey == "" {
commonFlags.TrustKey = filepath.Join(getDaemonConfDir(), defaultTrustKeyFile) commonFlags.TrustKey = filepath.Join(getDaemonConfDir(), defaultTrustKeyFile)
} }
cliConfig, err := loadDaemonCliConfig(cli.Config, cli.flags, commonFlags, *configFile)
if err != nil {
fmt.Fprint(os.Stderr, err)
os.Exit(1)
}
cli.Config = cliConfig
if cli.Config.Debug {
utils.EnableDebug()
}
if utils.ExperimentalBuild() { if utils.ExperimentalBuild() {
logrus.Warn("Running experimental build") logrus.Warn("Running experimental build")
@ -184,12 +202,18 @@ func (cli *DaemonCli) CmdDaemon(args ...string) error {
serverConfig = setPlatformServerConfig(serverConfig, cli.Config) serverConfig = setPlatformServerConfig(serverConfig, cli.Config)
defaultHost := opts.DefaultHost defaultHost := opts.DefaultHost
if commonFlags.TLSOptions != nil { if cli.Config.TLS {
if !commonFlags.TLSOptions.InsecureSkipVerify { tlsOptions := tlsconfig.Options{
// server requires and verifies client's certificate CAFile: cli.Config.TLSOptions.CAFile,
commonFlags.TLSOptions.ClientAuth = tls.RequireAndVerifyClientCert CertFile: cli.Config.TLSOptions.CertFile,
KeyFile: cli.Config.TLSOptions.KeyFile,
} }
tlsConfig, err := tlsconfig.Server(*commonFlags.TLSOptions)
if cli.Config.TLSVerify {
// server requires and verifies client's certificate
tlsOptions.ClientAuth = tls.RequireAndVerifyClientCert
}
tlsConfig, err := tlsconfig.Server(tlsOptions)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -197,22 +221,23 @@ func (cli *DaemonCli) CmdDaemon(args ...string) error {
defaultHost = opts.DefaultTLSHost defaultHost = opts.DefaultTLSHost
} }
if len(commonFlags.Hosts) == 0 { if len(cli.Config.Hosts) == 0 {
commonFlags.Hosts = make([]string, 1) cli.Config.Hosts = make([]string, 1)
} }
for i := 0; i < len(commonFlags.Hosts); i++ { for i := 0; i < len(cli.Config.Hosts); i++ {
var err error var err error
if commonFlags.Hosts[i], err = opts.ParseHost(defaultHost, commonFlags.Hosts[i]); err != nil { if cli.Config.Hosts[i], err = opts.ParseHost(defaultHost, cli.Config.Hosts[i]); err != nil {
logrus.Fatalf("error parsing -H %s : %v", commonFlags.Hosts[i], err) logrus.Fatalf("error parsing -H %s : %v", cli.Config.Hosts[i], err)
} }
}
for _, protoAddr := range commonFlags.Hosts { protoAddr := cli.Config.Hosts[i]
protoAddrParts := strings.SplitN(protoAddr, "://", 2) protoAddrParts := strings.SplitN(protoAddr, "://", 2)
if len(protoAddrParts) != 2 { if len(protoAddrParts) != 2 {
logrus.Fatalf("bad format %s, expected PROTO://ADDR", protoAddr) logrus.Fatalf("bad format %s, expected PROTO://ADDR", protoAddr)
} }
serverConfig.Addrs = append(serverConfig.Addrs, apiserver.Addr{Proto: protoAddrParts[0], Addr: protoAddrParts[1]}) serverConfig.Addrs = append(serverConfig.Addrs, apiserver.Addr{Proto: protoAddrParts[0], Addr: protoAddrParts[1]})
} }
api, err := apiserver.New(serverConfig) api, err := apiserver.New(serverConfig)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -245,18 +270,21 @@ func (cli *DaemonCli) CmdDaemon(args ...string) error {
api.InitRouters(d) api.InitRouters(d)
reload := func(config *daemon.Config) {
if err := d.Reload(config); err != nil {
logrus.Errorf("Error reconfiguring the daemon: %v", err)
return
}
api.Reload(config)
}
setupConfigReloadTrap(*configFile, cli.flags, reload)
// The serve API routine never exits unless an error occurs // The serve API routine never exits unless an error occurs
// We need to start it as a goroutine and wait on it so // We need to start it as a goroutine and wait on it so
// daemon doesn't exit // daemon doesn't exit
serveAPIWait := make(chan error) serveAPIWait := make(chan error)
go func() { go api.Wait(serveAPIWait)
if err := api.ServeAPI(); err != nil {
logrus.Errorf("ServeAPI error: %v", err)
serveAPIWait <- err
return
}
serveAPIWait <- nil
}()
signal.Trap(func() { signal.Trap(func() {
api.Close() api.Close()
@ -303,3 +331,34 @@ func shutdownDaemon(d *daemon.Daemon, timeout time.Duration) {
logrus.Error("Force shutdown daemon") logrus.Error("Force shutdown daemon")
} }
} }
func loadDaemonCliConfig(config *daemon.Config, daemonFlags *flag.FlagSet, commonConfig *cli.CommonFlags, configFile string) (*daemon.Config, error) {
config.Debug = commonConfig.Debug
config.Hosts = commonConfig.Hosts
config.LogLevel = commonConfig.LogLevel
config.TLS = commonConfig.TLS
config.TLSVerify = commonConfig.TLSVerify
config.TLSOptions = daemon.CommonTLSOptions{}
if commonConfig.TLSOptions != nil {
config.TLSOptions.CAFile = commonConfig.TLSOptions.CAFile
config.TLSOptions.CertFile = commonConfig.TLSOptions.CertFile
config.TLSOptions.KeyFile = commonConfig.TLSOptions.KeyFile
}
if configFile != "" {
c, err := daemon.MergeDaemonConfigurations(config, daemonFlags, configFile)
if err != nil {
if daemonFlags.IsSet(daemonConfigFileFlag) || !os.IsNotExist(err) {
return nil, fmt.Errorf("unable to configure the Docker daemon with file %s: %v\n", configFile, err)
}
}
// the merged configuration can be nil if the config file didn't exist.
// leave the current configuration as it is if when that happens.
if c != nil {
config = c
}
}
return config, nil
}

91
docker/daemon_test.go Normal file
View File

@ -0,0 +1,91 @@
// +build daemon
package main
import (
"io/ioutil"
"strings"
"testing"
"github.com/docker/docker/cli"
"github.com/docker/docker/daemon"
"github.com/docker/docker/opts"
"github.com/docker/docker/pkg/mflag"
"github.com/docker/go-connections/tlsconfig"
)
func TestLoadDaemonCliConfigWithoutOverriding(t *testing.T) {
c := &daemon.Config{}
common := &cli.CommonFlags{
Debug: true,
}
flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
loadedConfig, err := loadDaemonCliConfig(c, flags, common, "/tmp/fooobarbaz")
if err != nil {
t.Fatal(err)
}
if loadedConfig == nil {
t.Fatalf("expected configuration %v, got nil", c)
}
if !loadedConfig.Debug {
t.Fatalf("expected debug to be copied from the common flags, got false")
}
}
func TestLoadDaemonCliConfigWithTLS(t *testing.T) {
c := &daemon.Config{}
common := &cli.CommonFlags{
TLS: true,
TLSOptions: &tlsconfig.Options{
CAFile: "/tmp/ca.pem",
},
}
flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
loadedConfig, err := loadDaemonCliConfig(c, flags, common, "/tmp/fooobarbaz")
if err != nil {
t.Fatal(err)
}
if loadedConfig == nil {
t.Fatalf("expected configuration %v, got nil", c)
}
if loadedConfig.TLSOptions.CAFile != "/tmp/ca.pem" {
t.Fatalf("expected /tmp/ca.pem, got %s: %q", loadedConfig.TLSOptions.CAFile, loadedConfig)
}
}
func TestLoadDaemonCliConfigWithConflicts(t *testing.T) {
c := &daemon.Config{}
common := &cli.CommonFlags{}
f, err := ioutil.TempFile("", "docker-config-")
if err != nil {
t.Fatal(err)
}
configFile := f.Name()
f.Write([]byte(`{"labels": ["l3=foo"]}`))
f.Close()
var labels []string
flags := mflag.NewFlagSet("test", mflag.ContinueOnError)
flags.String([]string{daemonConfigFileFlag}, "", "")
flags.Var(opts.NewNamedListOptsRef("labels", &labels, opts.ValidateLabel), []string{"-label"}, "")
flags.Set(daemonConfigFileFlag, configFile)
if err := flags.Set("-label", "l1=bar"); err != nil {
t.Fatal(err)
}
if err := flags.Set("-label", "l2=baz"); err != nil {
t.Fatal(err)
}
_, err = loadDaemonCliConfig(c, flags, common, configFile)
if err == nil {
t.Fatalf("expected configuration error, got nil")
}
if !strings.Contains(err.Error(), "labels") {
t.Fatalf("expected labels conflict, got %v", err)
}
}

View File

@ -5,15 +5,19 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"os/signal"
"syscall" "syscall"
apiserver "github.com/docker/docker/api/server" apiserver "github.com/docker/docker/api/server"
"github.com/docker/docker/daemon" "github.com/docker/docker/daemon"
"github.com/docker/docker/pkg/mflag"
"github.com/docker/docker/pkg/system" "github.com/docker/docker/pkg/system"
_ "github.com/docker/docker/daemon/execdriver/native" _ "github.com/docker/docker/daemon/execdriver/native"
) )
const defaultDaemonConfigFile = "/etc/docker/daemon.json"
func setPlatformServerConfig(serverConfig *apiserver.Config, daemonCfg *daemon.Config) *apiserver.Config { func setPlatformServerConfig(serverConfig *apiserver.Config, daemonCfg *daemon.Config) *apiserver.Config {
serverConfig.SocketGroup = daemonCfg.SocketGroup serverConfig.SocketGroup = daemonCfg.SocketGroup
serverConfig.EnableCors = daemonCfg.EnableCors serverConfig.EnableCors = daemonCfg.EnableCors
@ -48,3 +52,14 @@ func setDefaultUmask() error {
func getDaemonConfDir() string { func getDaemonConfDir() string {
return "/etc/docker" return "/etc/docker"
} }
// setupConfigReloadTrap configures the USR2 signal to reload the configuration.
func setupConfigReloadTrap(configFile string, flags *mflag.FlagSet, reload func(*daemon.Config)) {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP)
go func() {
for range c {
daemon.ReloadConfiguration(configFile, flags, reload)
}
}()
}

View File

@ -3,12 +3,19 @@
package main package main
import ( import (
"fmt"
"os" "os"
"syscall"
"github.com/Sirupsen/logrus"
apiserver "github.com/docker/docker/api/server" apiserver "github.com/docker/docker/api/server"
"github.com/docker/docker/daemon" "github.com/docker/docker/daemon"
"github.com/docker/docker/pkg/mflag"
"github.com/docker/docker/pkg/system"
) )
var defaultDaemonConfigFile = os.Getenv("programdata") + string(os.PathSeparator) + "docker" + string(os.PathSeparator) + "config" + string(os.PathSeparator) + "daemon.json"
func setPlatformServerConfig(serverConfig *apiserver.Config, daemonCfg *daemon.Config) *apiserver.Config { func setPlatformServerConfig(serverConfig *apiserver.Config, daemonCfg *daemon.Config) *apiserver.Config {
return serverConfig return serverConfig
} }
@ -31,3 +38,20 @@ func getDaemonConfDir() string {
// notifySystem sends a message to the host when the server is ready to be used // notifySystem sends a message to the host when the server is ready to be used
func notifySystem() { func notifySystem() {
} }
// setupConfigReloadTrap configures a Win32 event to reload the configuration.
func setupConfigReloadTrap(configFile string, flags *mflag.FlagSet, reload func(*daemon.Config)) {
go func() {
sa := syscall.SecurityAttributes{
Length: 0,
}
ev := "Global\\docker-daemon-config-" + fmt.Sprint(os.Getpid())
if h, _ := system.CreateEvent(&sa, false, false, ev); h != 0 {
logrus.Debugf("Config reload - waiting signal at %s", ev)
for {
syscall.WaitForSingleObject(h, syscall.INFINITE)
daemon.ReloadConfiguration(configFile, flags, reload)
}
}
}()
}

View File

@ -27,6 +27,7 @@ weight = -1
--cluster-store="" URL of the distributed storage backend --cluster-store="" URL of the distributed storage backend
--cluster-advertise="" Address of the daemon instance on the cluster --cluster-advertise="" Address of the daemon instance on the cluster
--cluster-store-opt=map[] Set cluster options --cluster-store-opt=map[] Set cluster options
--config-file=/etc/docker/daemon.json Daemon configuration file
--dns=[] DNS server to use --dns=[] DNS server to use
--dns-opt=[] DNS options to use --dns-opt=[] DNS options to use
--dns-search=[] DNS search domains to use --dns-search=[] DNS search domains to use
@ -776,7 +777,7 @@ set like this:
/usr/local/bin/docker daemon -D -g /var/lib/docker -H unix:// > /var/lib/docker-machine/docker.log 2>&1 /usr/local/bin/docker daemon -D -g /var/lib/docker -H unix:// > /var/lib/docker-machine/docker.log 2>&1
# Default cgroup parent ## Default cgroup parent
The `--cgroup-parent` option allows you to set the default cgroup parent The `--cgroup-parent` option allows you to set the default cgroup parent
to use for containers. If this option is not set, it defaults to `/docker` for 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`
This setting can also be set per container, using the `--cgroup-parent` This setting can also be set per container, using the `--cgroup-parent`
option on `docker create` and `docker run`, and takes precedence over option on `docker create` and `docker run`, and takes precedence over
the `--cgroup-parent` option on the daemon. the `--cgroup-parent` option on the daemon.
## Daemon configuration file
The `--config-file` option allows you to set any configuration option
for the daemon in a JSON format. This file uses the same flag names as keys,
except for flags that allow several entries, where it uses the plural
of the flag name, e.g., `labels` for the `label` flag. By default,
docker tries to load a configuration file from `/etc/docker/daemon.json`
on Linux and `%programdata%\docker\config\daemon.json` on Windows.
The options set in the configuration file must not conflict with options set
via flags. The docker daemon fails to start if an option is duplicated between
the file and the flags, regardless their value. We do this to avoid
silently ignore changes introduced in configuration reloads.
For example, the daemon fails to start if you set daemon labels
in the configuration file and also set daemon labels via the `--label` flag.
Options that are not present in the file are ignored when the daemon starts.
This is a full example of the allowed configuration options in the file:
```json
{
"authorization-plugins": [],
"dns": [],
"dns-opts": [],
"dns-search": [],
"exec-opts": [],
"exec-root": "",
"storage-driver": "",
"storage-opts": "",
"labels": [],
"log-config": {
"log-driver": "",
"log-opts": []
},
"mtu": 0,
"pidfile": "",
"graph": "",
"cluster-store": "",
"cluster-store-opts": [],
"cluster-advertise": "",
"debug": true,
"hosts": [],
"log-level": "",
"tls": true,
"tls-verify": true,
"tls-opts": {
"tlscacert": "",
"tlscert": "",
"tlskey": ""
},
"api-cors-headers": "",
"selinux-enabled": false,
"userns-remap": "",
"group": "",
"cgroup-parent": "",
"default-ulimits": {}
}
```
### Configuration reloading
Some options can be reconfigured when the daemon is running without requiring
to restart the process. We use the `SIGHUP` signal in Linux to reload, and a global event
in Windows with the key `Global\docker-daemon-config-$PID`. The options can
be modified in the configuration file but still will check for conflicts with
the provided flags. The daemon fails to reconfigure itself
if there are conflicts, but it won't stop execution.
The list of currently supported options that can be reconfigured is this:
- `debug`: it changes the daemon to debug mode when set to true.
- `label`: it replaces the daemon labels with a new set of labels.
- `cluster-store`: it reloads the discovery store with the new address.
- `cluster-store-opts`: it uses the new options to reload the discovery store.
- `cluster-advertise`: it modifies the address advertised after reloading.

View File

@ -133,7 +133,7 @@ func (s *DockerSuite) TestHelpTextVerify(c *check.C) {
// Check each line for lots of stuff // Check each line for lots of stuff
lines := strings.Split(out, "\n") lines := strings.Split(out, "\n")
for _, line := range lines { for _, line := range lines {
c.Assert(len(line), checker.LessOrEqualThan, 103, check.Commentf("Help for %q is too long:\n%s", cmd, line)) c.Assert(len(line), checker.LessOrEqualThan, 107, check.Commentf("Help for %q is too long:\n%s", cmd, line))
if scanForHome && strings.Contains(line, `"`+home) { if scanForHome && strings.Contains(line, `"`+home) {
c.Fatalf("Help for %q should use ~ instead of %q on:\n%s", c.Fatalf("Help for %q should use ~ instead of %q on:\n%s",

View File

@ -14,6 +14,7 @@ docker-daemon - Enable daemon mode
[**--cluster-store**[=*[]*]] [**--cluster-store**[=*[]*]]
[**--cluster-advertise**[=*[]*]] [**--cluster-advertise**[=*[]*]]
[**--cluster-store-opt**[=*map[]*]] [**--cluster-store-opt**[=*map[]*]]
[**--config-file**[=*/etc/docker/daemon.json*]]
[**-D**|**--debug**] [**-D**|**--debug**]
[**--default-gateway**[=*DEFAULT-GATEWAY*]] [**--default-gateway**[=*DEFAULT-GATEWAY*]]
[**--default-gateway-v6**[=*DEFAULT-GATEWAY-V6*]] [**--default-gateway-v6**[=*DEFAULT-GATEWAY-V6*]]
@ -96,6 +97,9 @@ format.
**--cluster-store-opt**="" **--cluster-store-opt**=""
Specifies options for the Key/Value store. Specifies options for the Key/Value store.
**--config-file**="/etc/docker/daemon.json"
Specifies the JSON file path to load the configuration from.
**-D**, **--debug**=*true*|*false* **-D**, **--debug**=*true*|*false*
Enable debug mode. Default is false. Enable debug mode. Default is false.

View File

@ -100,6 +100,35 @@ func (opts *ListOpts) Len() int {
return len((*opts.values)) return len((*opts.values))
} }
// NamedOption is an interface that list and map options
// with names implement.
type NamedOption interface {
Name() string
}
// NamedListOpts is a ListOpts with a configuration name.
// This struct is useful to keep reference to the assigned
// field name in the internal configuration struct.
type NamedListOpts struct {
name string
ListOpts
}
var _ NamedOption = &NamedListOpts{}
// NewNamedListOptsRef creates a reference to a new NamedListOpts struct.
func NewNamedListOptsRef(name string, values *[]string, validator ValidatorFctType) *NamedListOpts {
return &NamedListOpts{
name: name,
ListOpts: *NewListOptsRef(values, validator),
}
}
// Name returns the name of the NamedListOpts in the configuration.
func (o *NamedListOpts) Name() string {
return o.name
}
//MapOpts holds a map of values and a validation function. //MapOpts holds a map of values and a validation function.
type MapOpts struct { type MapOpts struct {
values map[string]string values map[string]string
@ -145,6 +174,29 @@ func NewMapOpts(values map[string]string, validator ValidatorFctType) *MapOpts {
} }
} }
// NamedMapOpts is a MapOpts struct with a configuration name.
// This struct is useful to keep reference to the assigned
// field name in the internal configuration struct.
type NamedMapOpts struct {
name string
MapOpts
}
var _ NamedOption = &NamedMapOpts{}
// NewNamedMapOpts creates a reference to a new NamedMapOpts struct.
func NewNamedMapOpts(name string, values map[string]string, validator ValidatorFctType) *NamedMapOpts {
return &NamedMapOpts{
name: name,
MapOpts: *NewMapOpts(values, validator),
}
}
// Name returns the name of the NamedMapOpts in the configuration.
func (o *NamedMapOpts) Name() string {
return o.name
}
// ValidatorFctType defines a validator function that returns a validated string and/or an error. // ValidatorFctType defines a validator function that returns a validated string and/or an error.
type ValidatorFctType func(val string) (string, error) type ValidatorFctType func(val string) (string, error)

View File

@ -198,3 +198,35 @@ func logOptsValidator(val string) (string, error) {
} }
return "", fmt.Errorf("invalid key %s", vals[0]) return "", fmt.Errorf("invalid key %s", vals[0])
} }
func TestNamedListOpts(t *testing.T) {
var v []string
o := NewNamedListOptsRef("foo-name", &v, nil)
o.Set("foo")
if o.String() != "[foo]" {
t.Errorf("%s != [foo]", o.String())
}
if o.Name() != "foo-name" {
t.Errorf("%s != foo-name", o.Name())
}
if len(v) != 1 {
t.Errorf("expected foo to be in the values, got %v", v)
}
}
func TestNamedMapOpts(t *testing.T) {
tmpMap := make(map[string]string)
o := NewNamedMapOpts("max-name", tmpMap, nil)
o.Set("max-size=1")
if o.String() != "map[max-size:1]" {
t.Errorf("%s != [map[max-size:1]", o.String())
}
if o.Name() != "max-name" {
t.Errorf("%s != max-name", o.Name())
}
if _, exist := tmpMap["max-size"]; !exist {
t.Errorf("expected map-size to be in the values, got %v", tmpMap)
}
}

View File

@ -12,12 +12,8 @@ import (
var ( var (
// Backends is a global map of discovery backends indexed by their // Backends is a global map of discovery backends indexed by their
// associated scheme. // associated scheme.
backends map[string]Backend
)
func init() {
backends = make(map[string]Backend) backends = make(map[string]Backend)
} )
// Register makes a discovery backend available by the provided scheme. // Register makes a discovery backend available by the provided scheme.
// If Register is called twice with the same scheme an error is returned. // If Register is called twice with the same scheme an error is returned.
@ -42,7 +38,7 @@ func parse(rawurl string) (string, string) {
// ParseAdvertise parses the --cluster-advertise daemon config which accepts // ParseAdvertise parses the --cluster-advertise daemon config which accepts
// <ip-address>:<port> or <interface-name>:<port> // <ip-address>:<port> or <interface-name>:<port>
func ParseAdvertise(store, advertise string) (string, error) { func ParseAdvertise(advertise string) (string, error) {
var ( var (
iface *net.Interface iface *net.Interface
addrs []net.Addr addrs []net.Addr

View File

@ -25,6 +25,7 @@ func Init() {
// Initialize sets the heartbeat for the memory backend. // Initialize sets the heartbeat for the memory backend.
func (s *Discovery) Initialize(_ string, heartbeat time.Duration, _ time.Duration, _ map[string]string) error { func (s *Discovery) Initialize(_ string, heartbeat time.Duration, _ time.Duration, _ map[string]string) error {
s.heartbeat = heartbeat s.heartbeat = heartbeat
s.values = make([]string, 0)
return nil return nil
} }

26
utils/debug.go Normal file
View File

@ -0,0 +1,26 @@
package utils
import (
"os"
"github.com/Sirupsen/logrus"
)
// EnableDebug sets the DEBUG env var to true
// and makes the logger to log at debug level.
func EnableDebug() {
os.Setenv("DEBUG", "1")
logrus.SetLevel(logrus.DebugLevel)
}
// DisableDebug sets the DEBUG env var to false
// and makes the logger to log at info level.
func DisableDebug() {
os.Setenv("DEBUG", "")
logrus.SetLevel(logrus.InfoLevel)
}
// IsDebugEnabled checks whether the debug flag is set or not.
func IsDebugEnabled() bool {
return os.Getenv("DEBUG") != ""
}