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"
"net"
"net/http"
"os"
"strings"
"github.com/Sirupsen/logrus"
@ -42,10 +41,11 @@ type Config struct {
// Server contains instance details for the server
type Server struct {
cfg *Config
servers []*HTTPServer
routers []router.Router
authZPlugins []authorization.Plugin
cfg *Config
servers []*HTTPServer
routers []router.Router
authZPlugins []authorization.Plugin
routerSwapper *routerSwapper
}
// 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
// with Server method for each. It sets CreateMux() as Handler also.
func (s *Server) ServeAPI() error {
// serveAPI loops through all initialized servers and spawns goroutine
// with Server method for each. It sets createMux() as Handler also.
func (s *Server) serveAPI() error {
s.initRouterSwapper()
var chErrors = make(chan error, len(s.servers))
for _, srv := range s.servers {
srv.srv.Handler = s.CreateMux()
srv.srv.Handler = s.routerSwapper
go func(srv *HTTPServer) {
var err error
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)
}
// 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
func (s *Server) CreateMux() *mux.Router {
func (s *Server) createMux() *mux.Router {
m := mux.NewRouter()
if os.Getenv("DEBUG") != "" {
if utils.IsDebugEnabled() {
profilerSetup(m, "/debug/")
}
@ -207,3 +209,36 @@ func (s *Server) CreateMux() *mux.Router {
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
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"strings"
"sync"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/opts"
"github.com/docker/docker/pkg/discovery"
flag "github.com/docker/docker/pkg/mflag"
"github.com/docker/engine-api/types/container"
"github.com/imdario/mergo"
)
const (
@ -11,42 +21,69 @@ const (
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
// 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 {
AuthorizationPlugins []string // AuthorizationPlugins holds list of authorization plugins
AutoRestart bool
Bridge bridgeConfig // Bridge holds bridge network specific configuration.
Context map[string][]string
DisableBridge bool
DNS []string
DNSOptions []string
DNSSearch []string
ExecOptions []string
ExecRoot string
GraphDriver string
GraphOptions []string
Labels []string
LogConfig container.LogConfig
Mtu int
Pidfile string
RemappedRoot string
Root string
TrustKeyPath string
AuthorizationPlugins []string `json:"authorization-plugins,omitempty"` // AuthorizationPlugins holds list of authorization plugins
AutoRestart bool `json:"-"`
Bridge bridgeConfig `json:"-"` // Bridge holds bridge network specific configuration.
Context map[string][]string `json:"-"`
DisableBridge bool `json:"-"`
DNS []string `json:"dns,omitempty"`
DNSOptions []string `json:"dns-opts,omitempty"`
DNSSearch []string `json:"dns-search,omitempty"`
ExecOptions []string `json:"exec-opts,omitempty"`
ExecRoot string `json:"exec-root,omitempty"`
GraphDriver string `json:"storage-driver,omitempty"`
GraphOptions []string `json:"storage-opts,omitempty"`
Labels []string `json:"labels,omitempty"`
LogConfig LogConfig `json:"log-config,omitempty"`
Mtu int `json:"mtu,omitempty"`
Pidfile string `json:"pidfile,omitempty"`
Root string `json:"graph,omitempty"`
TrustKeyPath string `json:"-"`
// 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
// mechanism.
ClusterStore string
ClusterStore string `json:"cluster-store,omitempty"`
// ClusterOpts is used to pass options to the discovery package for tuning libkv settings, such
// 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
// discovery. This should be a 'host:port' combination on which that daemon instance is
// 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
@ -54,9 +91,9 @@ type CommonConfig struct {
// Subsequent calls to `flag.Parse` will populate config with values parsed
// from the command-line.
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.NewListOptsRef(&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("storage-opts", &config.GraphOptions, nil), []string{"-storage-opt"}, usageFn("Set storage driver options"))
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.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.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"))
@ -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"))
// 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.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.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.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.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.
// 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 {
CommonConfig
// Fields below here are platform specific.
CorsHeaders string
EnableCors bool
EnableSelinuxSupport bool
RemappedRoot string
SocketGroup string
CgroupParent string
Ulimits map[string]*units.Ulimit
CorsHeaders string `json:"api-cors-headers,omitempty"`
EnableCors bool `json:"api-enable-cors,omitempty"`
EnableSelinuxSupport bool `json:"selinux-enabled,omitempty"`
RemappedRoot string `json:"userns-remap,omitempty"`
SocketGroup string `json:"group,omitempty"`
CgroupParent string `json:"cgroup-parent,omitempty"`
Ulimits map[string]*units.Ulimit `json:"default-ulimits,omitempty"`
}
// bridgeConfig stores all the bridge driver specific

View File

@ -46,7 +46,6 @@ import (
"github.com/docker/docker/layer"
"github.com/docker/docker/migrate/v1"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/discovery"
"github.com/docker/docker/pkg/fileutils"
"github.com/docker/docker/pkg/graphdb"
"github.com/docker/docker/pkg/idtools"
@ -155,7 +154,7 @@ type Daemon struct {
EventsService *events.Events
netController libnetwork.NetworkController
volumes *store.VolumeStore
discoveryWatcher discovery.Watcher
discoveryWatcher discoveryReloader
root string
seccompEnabled bool
shutdown bool
@ -292,7 +291,7 @@ func (daemon *Daemon) Register(container *container.Container) error {
func (daemon *Daemon) restore() error {
var (
debug = os.Getenv("DEBUG") != ""
debug = utils.IsDebugEnabled()
currentDriver = daemon.GraphDriverName()
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
// initialized, the daemon is registered and we can store the discovery backend as its read-only
// DiscoveryWatcher version.
if config.ClusterStore != "" && config.ClusterAdvertise != "" {
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")
if err := d.initDiscovery(config); err != nil {
return nil, err
}
d.netController, err = d.initNetworkController(config)
@ -815,7 +803,10 @@ func NewDaemon(config *Config, registryService *registry.Service) (daemon *Daemo
d.configStore = config
d.execDriver = ed
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.EventsService = eventsService
d.volumes = volStore
@ -1521,6 +1512,76 @@ func (daemon *Daemon) newBaseContainer(id string) *container.Container {
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 {
n := &libcontainer.NetworkInterface{Name: name}
n.RxBytes = stats.RxBytes

View File

@ -4,9 +4,13 @@ import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
"time"
"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/truncindex"
"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
import (
"errors"
"fmt"
"reflect"
"strconv"
"time"
@ -19,6 +21,24 @@ const (
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) {
var (
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
// 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) {
heartbeat, ttl, err := discoveryOpts(clusterOpts)
func initDiscovery(backendAddress, advertiseAddress string, clusterOpts map[string]string) (discoveryReloader, error) {
heartbeat, backend, err := parseDiscoveryOptions(backendAddress, clusterOpts)
if err != nil {
return nil, err
}
discoveryBackend, err := discovery.New(backend, heartbeat, ttl, clusterOpts)
if err != nil {
return nil, err
reloader := &daemonDiscoveryReloader{
backend: backend,
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,
// but we never actually Watch() for nodes appearing and disappearing for the moment.
go registrationLoop(discoveryBackend, address, heartbeat)
return discoveryBackend, nil
reloader.advertise(advertiseAddress)
return reloader, nil
}
func registerAddr(backend discovery.Backend, addr string) {
if err := backend.Register(addr); err != nil {
func (d *daemonDiscoveryReloader) advertise(address string) {
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)
}
}
// 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
// requires regular heartbeats.
func registrationLoop(discoveryBackend discovery.Backend, address string, heartbeat time.Duration) {
registerAddr(discoveryBackend, address)
for range time.Tick(heartbeat) {
registerAddr(discoveryBackend, address)
func (d *daemonDiscoveryReloader) advertiseHeartbeat(address string) {
for {
select {
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)
}
}
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,
BridgeNfIptables: !sysInfo.BridgeNfCallIptablesDisabled,
BridgeNfIP6tables: !sysInfo.BridgeNfCallIP6tablesDisabled,
Debug: os.Getenv("DEBUG") != "",
Debug: utils.IsDebugEnabled(),
NFd: fileutils.GetTotalUsedFds(),
NGoroutines: runtime.NumGoroutine(),
SystemTime: time.Now().Format(time.RFC3339Nano),

View File

@ -21,7 +21,6 @@ const (
)
var (
daemonFlags *flag.FlagSet
commonFlags = &cli.CommonFlags{FlagSet: new(flag.FlagSet)}
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.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() {
@ -67,11 +66,6 @@ func postParseCommon() {
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
// 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

View File

@ -30,23 +30,34 @@ import (
"github.com/docker/go-connections/tlsconfig"
)
const daemonUsage = " docker daemon [ --help | ... ]\n"
const (
daemonUsage = " docker daemon [ --help | ... ]\n"
daemonConfigFileFlag = "-config-file"
)
var (
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 absentFromHelp(string) string { return "" }
// NewDaemonCli returns a pre-configured daemon CLI
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?
daemonConfig := new(daemon.Config)
daemonConfig.LogConfig.Config = make(map[string]string)
daemonConfig.ClusterOpts = make(map[string]string)
daemonConfig.InstallFlags(daemonFlags, presentInHelp)
daemonConfig.InstallFlags(flag.CommandLine, absentFromHelp)
registryOptions := new(registry.Options)
@ -57,6 +68,7 @@ func NewDaemonCli() *DaemonCli {
return &DaemonCli{
Config: daemonConfig,
registryOptions: registryOptions,
flags: daemonFlags,
}
}
@ -101,12 +113,6 @@ func migrateKey() (err error) {
return nil
}
// DaemonCli represents the daemon CLI.
type DaemonCli struct {
*daemon.Config
registryOptions *registry.Options
}
func getGlobalFlag() (globalFlag *flag.Flag) {
defer func() {
if x := recover(); x != nil {
@ -136,15 +142,27 @@ func (cli *DaemonCli) CmdDaemon(args ...string) error {
os.Exit(1)
} else {
// 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()
if commonFlags.TrustKey == "" {
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() {
logrus.Warn("Running experimental build")
@ -184,12 +202,18 @@ func (cli *DaemonCli) CmdDaemon(args ...string) error {
serverConfig = setPlatformServerConfig(serverConfig, cli.Config)
defaultHost := opts.DefaultHost
if commonFlags.TLSOptions != nil {
if !commonFlags.TLSOptions.InsecureSkipVerify {
// server requires and verifies client's certificate
commonFlags.TLSOptions.ClientAuth = tls.RequireAndVerifyClientCert
if cli.Config.TLS {
tlsOptions := tlsconfig.Options{
CAFile: cli.Config.TLSOptions.CAFile,
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 {
logrus.Fatal(err)
}
@ -197,22 +221,23 @@ func (cli *DaemonCli) CmdDaemon(args ...string) error {
defaultHost = opts.DefaultTLSHost
}
if len(commonFlags.Hosts) == 0 {
commonFlags.Hosts = make([]string, 1)
if len(cli.Config.Hosts) == 0 {
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
if commonFlags.Hosts[i], err = opts.ParseHost(defaultHost, commonFlags.Hosts[i]); err != nil {
logrus.Fatalf("error parsing -H %s : %v", commonFlags.Hosts[i], err)
if cli.Config.Hosts[i], err = opts.ParseHost(defaultHost, cli.Config.Hosts[i]); err != nil {
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)
if len(protoAddrParts) != 2 {
logrus.Fatalf("bad format %s, expected PROTO://ADDR", protoAddr)
}
serverConfig.Addrs = append(serverConfig.Addrs, apiserver.Addr{Proto: protoAddrParts[0], Addr: protoAddrParts[1]})
}
api, err := apiserver.New(serverConfig)
if err != nil {
logrus.Fatal(err)
@ -245,18 +270,21 @@ func (cli *DaemonCli) CmdDaemon(args ...string) error {
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
// We need to start it as a goroutine and wait on it so
// daemon doesn't exit
serveAPIWait := make(chan error)
go func() {
if err := api.ServeAPI(); err != nil {
logrus.Errorf("ServeAPI error: %v", err)
serveAPIWait <- err
return
}
serveAPIWait <- nil
}()
go api.Wait(serveAPIWait)
signal.Trap(func() {
api.Close()
@ -303,3 +331,34 @@ func shutdownDaemon(d *daemon.Daemon, timeout time.Duration) {
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 (
"fmt"
"os"
"os/signal"
"syscall"
apiserver "github.com/docker/docker/api/server"
"github.com/docker/docker/daemon"
"github.com/docker/docker/pkg/mflag"
"github.com/docker/docker/pkg/system"
_ "github.com/docker/docker/daemon/execdriver/native"
)
const defaultDaemonConfigFile = "/etc/docker/daemon.json"
func setPlatformServerConfig(serverConfig *apiserver.Config, daemonCfg *daemon.Config) *apiserver.Config {
serverConfig.SocketGroup = daemonCfg.SocketGroup
serverConfig.EnableCors = daemonCfg.EnableCors
@ -48,3 +52,14 @@ func setDefaultUmask() error {
func getDaemonConfDir() string {
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
import (
"fmt"
"os"
"syscall"
"github.com/Sirupsen/logrus"
apiserver "github.com/docker/docker/api/server"
"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 {
return serverConfig
}
@ -31,3 +38,20 @@ func getDaemonConfDir() string {
// notifySystem sends a message to the host when the server is ready to be used
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-advertise="" Address of the daemon instance on the cluster
--cluster-store-opt=map[] Set cluster options
--config-file=/etc/docker/daemon.json Daemon configuration file
--dns=[] DNS server to use
--dns-opt=[] DNS options 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
# Default cgroup parent
## 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
@ -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`
option on `docker create` and `docker run`, and takes precedence over
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
lines := strings.Split(out, "\n")
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) {
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-advertise**[=*[]*]]
[**--cluster-store-opt**[=*map[]*]]
[**--config-file**[=*/etc/docker/daemon.json*]]
[**-D**|**--debug**]
[**--default-gateway**[=*DEFAULT-GATEWAY*]]
[**--default-gateway-v6**[=*DEFAULT-GATEWAY-V6*]]
@ -96,6 +97,9 @@ format.
**--cluster-store-opt**=""
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*
Enable debug mode. Default is false.

View File

@ -100,6 +100,35 @@ func (opts *ListOpts) Len() int {
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.
type MapOpts struct {
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.
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])
}
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 (
// Backends is a global map of discovery backends indexed by their
// associated scheme.
backends map[string]Backend
)
func init() {
backends = make(map[string]Backend)
}
)
// Register makes a discovery backend available by the provided scheme.
// 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
// <ip-address>:<port> or <interface-name>:<port>
func ParseAdvertise(store, advertise string) (string, error) {
func ParseAdvertise(advertise string) (string, error) {
var (
iface *net.Interface
addrs []net.Addr

View File

@ -25,6 +25,7 @@ func Init() {
// Initialize sets the heartbeat for the memory backend.
func (s *Discovery) Initialize(_ string, heartbeat time.Duration, _ time.Duration, _ map[string]string) error {
s.heartbeat = heartbeat
s.values = make([]string, 0)
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") != ""
}