github.com/Shopify/docker@v1.13.1/daemon/config.go (about) 1 package daemon 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "runtime" 11 "strings" 12 "sync" 13 14 "github.com/Sirupsen/logrus" 15 "github.com/docker/docker/opts" 16 "github.com/docker/docker/pkg/discovery" 17 "github.com/docker/docker/registry" 18 "github.com/imdario/mergo" 19 "github.com/spf13/pflag" 20 ) 21 22 const ( 23 // defaultMaxConcurrentDownloads is the default value for 24 // maximum number of downloads that 25 // may take place at a time for each pull. 26 defaultMaxConcurrentDownloads = 3 27 // defaultMaxConcurrentUploads is the default value for 28 // maximum number of uploads that 29 // may take place at a time for each push. 30 defaultMaxConcurrentUploads = 5 31 // stockRuntimeName is the reserved name/alias used to represent the 32 // OCI runtime being shipped with the docker daemon package. 33 stockRuntimeName = "runc" 34 ) 35 36 const ( 37 defaultNetworkMtu = 1500 38 disableNetworkBridge = "none" 39 ) 40 41 const ( 42 defaultShutdownTimeout = 15 43 ) 44 45 // flatOptions contains configuration keys 46 // that MUST NOT be parsed as deep structures. 47 // Use this to differentiate these options 48 // with others like the ones in CommonTLSOptions. 49 var flatOptions = map[string]bool{ 50 "cluster-store-opts": true, 51 "log-opts": true, 52 "runtimes": true, 53 "default-ulimits": true, 54 } 55 56 // LogConfig represents the default log configuration. 57 // It includes json tags to deserialize configuration from a file 58 // using the same names that the flags in the command line use. 59 type LogConfig struct { 60 Type string `json:"log-driver,omitempty"` 61 Config map[string]string `json:"log-opts,omitempty"` 62 } 63 64 // commonBridgeConfig stores all the platform-common bridge driver specific 65 // configuration. 66 type commonBridgeConfig struct { 67 Iface string `json:"bridge,omitempty"` 68 FixedCIDR string `json:"fixed-cidr,omitempty"` 69 } 70 71 // CommonTLSOptions defines TLS configuration for the daemon server. 72 // It includes json tags to deserialize configuration from a file 73 // using the same names that the flags in the command line use. 74 type CommonTLSOptions struct { 75 CAFile string `json:"tlscacert,omitempty"` 76 CertFile string `json:"tlscert,omitempty"` 77 KeyFile string `json:"tlskey,omitempty"` 78 } 79 80 // CommonConfig defines the configuration of a docker daemon which is 81 // common across platforms. 82 // It includes json tags to deserialize configuration from a file 83 // using the same names that the flags in the command line use. 84 type CommonConfig struct { 85 AuthorizationPlugins []string `json:"authorization-plugins,omitempty"` // AuthorizationPlugins holds list of authorization plugins 86 AutoRestart bool `json:"-"` 87 Context map[string][]string `json:"-"` 88 DisableBridge bool `json:"-"` 89 DNS []string `json:"dns,omitempty"` 90 DNSOptions []string `json:"dns-opts,omitempty"` 91 DNSSearch []string `json:"dns-search,omitempty"` 92 ExecOptions []string `json:"exec-opts,omitempty"` 93 GraphDriver string `json:"storage-driver,omitempty"` 94 GraphOptions []string `json:"storage-opts,omitempty"` 95 Labels []string `json:"labels,omitempty"` 96 Mtu int `json:"mtu,omitempty"` 97 Pidfile string `json:"pidfile,omitempty"` 98 RawLogs bool `json:"raw-logs,omitempty"` 99 Root string `json:"graph,omitempty"` 100 SocketGroup string `json:"group,omitempty"` 101 TrustKeyPath string `json:"-"` 102 CorsHeaders string `json:"api-cors-header,omitempty"` 103 EnableCors bool `json:"api-enable-cors,omitempty"` 104 105 // LiveRestoreEnabled determines whether we should keep containers 106 // alive upon daemon shutdown/start 107 LiveRestoreEnabled bool `json:"live-restore,omitempty"` 108 109 // ClusterStore is the storage backend used for the cluster information. It is used by both 110 // multihost networking (to store networks and endpoints information) and by the node discovery 111 // mechanism. 112 ClusterStore string `json:"cluster-store,omitempty"` 113 114 // ClusterOpts is used to pass options to the discovery package for tuning libkv settings, such 115 // as TLS configuration settings. 116 ClusterOpts map[string]string `json:"cluster-store-opts,omitempty"` 117 118 // ClusterAdvertise is the network endpoint that the Engine advertises for the purpose of node 119 // discovery. This should be a 'host:port' combination on which that daemon instance is 120 // reachable by other hosts. 121 ClusterAdvertise string `json:"cluster-advertise,omitempty"` 122 123 // MaxConcurrentDownloads is the maximum number of downloads that 124 // may take place at a time for each pull. 125 MaxConcurrentDownloads *int `json:"max-concurrent-downloads,omitempty"` 126 127 // MaxConcurrentUploads is the maximum number of uploads that 128 // may take place at a time for each push. 129 MaxConcurrentUploads *int `json:"max-concurrent-uploads,omitempty"` 130 131 // ShutdownTimeout is the timeout value (in seconds) the daemon will wait for the container 132 // to stop when daemon is being shutdown 133 ShutdownTimeout int `json:"shutdown-timeout,omitempty"` 134 135 Debug bool `json:"debug,omitempty"` 136 Hosts []string `json:"hosts,omitempty"` 137 LogLevel string `json:"log-level,omitempty"` 138 TLS bool `json:"tls,omitempty"` 139 TLSVerify bool `json:"tlsverify,omitempty"` 140 141 // Embedded structs that allow config 142 // deserialization without the full struct. 143 CommonTLSOptions 144 145 // SwarmDefaultAdvertiseAddr is the default host/IP or network interface 146 // to use if a wildcard address is specified in the ListenAddr value 147 // given to the /swarm/init endpoint and no advertise address is 148 // specified. 149 SwarmDefaultAdvertiseAddr string `json:"swarm-default-advertise-addr"` 150 MetricsAddress string `json:"metrics-addr"` 151 152 LogConfig 153 bridgeConfig // bridgeConfig holds bridge network specific configuration. 154 registry.ServiceOptions 155 156 reloadLock sync.Mutex 157 valuesSet map[string]interface{} 158 159 Experimental bool `json:"experimental"` // Experimental indicates whether experimental features should be exposed or not 160 } 161 162 // InstallCommonFlags adds flags to the pflag.FlagSet to configure the daemon 163 func (config *Config) InstallCommonFlags(flags *pflag.FlagSet) { 164 var maxConcurrentDownloads, maxConcurrentUploads int 165 166 config.ServiceOptions.InstallCliFlags(flags) 167 168 flags.Var(opts.NewNamedListOptsRef("storage-opts", &config.GraphOptions, nil), "storage-opt", "Storage driver options") 169 flags.Var(opts.NewNamedListOptsRef("authorization-plugins", &config.AuthorizationPlugins, nil), "authorization-plugin", "Authorization plugins to load") 170 flags.Var(opts.NewNamedListOptsRef("exec-opts", &config.ExecOptions, nil), "exec-opt", "Runtime execution options") 171 flags.StringVarP(&config.Pidfile, "pidfile", "p", defaultPidFile, "Path to use for daemon PID file") 172 flags.StringVarP(&config.Root, "graph", "g", defaultGraph, "Root of the Docker runtime") 173 flags.BoolVarP(&config.AutoRestart, "restart", "r", true, "--restart on the daemon has been deprecated in favor of --restart policies on docker run") 174 flags.MarkDeprecated("restart", "Please use a restart policy on docker run") 175 flags.StringVarP(&config.GraphDriver, "storage-driver", "s", "", "Storage driver to use") 176 flags.IntVar(&config.Mtu, "mtu", 0, "Set the containers network MTU") 177 flags.BoolVar(&config.RawLogs, "raw-logs", false, "Full timestamps without ANSI coloring") 178 // FIXME: why the inconsistency between "hosts" and "sockets"? 179 flags.Var(opts.NewListOptsRef(&config.DNS, opts.ValidateIPAddress), "dns", "DNS server to use") 180 flags.Var(opts.NewNamedListOptsRef("dns-opts", &config.DNSOptions, nil), "dns-opt", "DNS options to use") 181 flags.Var(opts.NewListOptsRef(&config.DNSSearch, opts.ValidateDNSSearch), "dns-search", "DNS search domains to use") 182 flags.Var(opts.NewNamedListOptsRef("labels", &config.Labels, opts.ValidateLabel), "label", "Set key=value labels to the daemon") 183 flags.StringVar(&config.LogConfig.Type, "log-driver", "json-file", "Default driver for container logs") 184 flags.Var(opts.NewNamedMapOpts("log-opts", config.LogConfig.Config, nil), "log-opt", "Default log driver options for containers") 185 flags.StringVar(&config.ClusterAdvertise, "cluster-advertise", "", "Address or interface name to advertise") 186 flags.StringVar(&config.ClusterStore, "cluster-store", "", "URL of the distributed storage backend") 187 flags.Var(opts.NewNamedMapOpts("cluster-store-opts", config.ClusterOpts, nil), "cluster-store-opt", "Set cluster store options") 188 flags.StringVar(&config.CorsHeaders, "api-cors-header", "", "Set CORS headers in the Engine API") 189 flags.IntVar(&maxConcurrentDownloads, "max-concurrent-downloads", defaultMaxConcurrentDownloads, "Set the max concurrent downloads for each pull") 190 flags.IntVar(&maxConcurrentUploads, "max-concurrent-uploads", defaultMaxConcurrentUploads, "Set the max concurrent uploads for each push") 191 flags.IntVar(&config.ShutdownTimeout, "shutdown-timeout", defaultShutdownTimeout, "Set the default shutdown timeout") 192 193 flags.StringVar(&config.SwarmDefaultAdvertiseAddr, "swarm-default-advertise-addr", "", "Set default address or interface for swarm advertised address") 194 flags.BoolVar(&config.Experimental, "experimental", false, "Enable experimental features") 195 196 flags.StringVar(&config.MetricsAddress, "metrics-addr", "", "Set default address and port to serve the metrics api on") 197 198 config.MaxConcurrentDownloads = &maxConcurrentDownloads 199 config.MaxConcurrentUploads = &maxConcurrentUploads 200 } 201 202 // IsValueSet returns true if a configuration value 203 // was explicitly set in the configuration file. 204 func (config *Config) IsValueSet(name string) bool { 205 if config.valuesSet == nil { 206 return false 207 } 208 _, ok := config.valuesSet[name] 209 return ok 210 } 211 212 // NewConfig returns a new fully initialized Config struct 213 func NewConfig() *Config { 214 config := Config{} 215 config.LogConfig.Config = make(map[string]string) 216 config.ClusterOpts = make(map[string]string) 217 218 if runtime.GOOS != "linux" { 219 config.V2Only = true 220 } 221 return &config 222 } 223 224 func parseClusterAdvertiseSettings(clusterStore, clusterAdvertise string) (string, error) { 225 if runtime.GOOS == "solaris" && (clusterAdvertise != "" || clusterStore != "") { 226 return "", errors.New("Cluster Advertise Settings not supported on Solaris") 227 } 228 if clusterAdvertise == "" { 229 return "", errDiscoveryDisabled 230 } 231 if clusterStore == "" { 232 return "", fmt.Errorf("invalid cluster configuration. --cluster-advertise must be accompanied by --cluster-store configuration") 233 } 234 235 advertise, err := discovery.ParseAdvertise(clusterAdvertise) 236 if err != nil { 237 return "", fmt.Errorf("discovery advertise parsing failed (%v)", err) 238 } 239 return advertise, nil 240 } 241 242 // GetConflictFreeLabels validate Labels for conflict 243 // In swarm the duplicates for labels are removed 244 // so we only take same values here, no conflict values 245 // If the key-value is the same we will only take the last label 246 func GetConflictFreeLabels(labels []string) ([]string, error) { 247 labelMap := map[string]string{} 248 for _, label := range labels { 249 stringSlice := strings.SplitN(label, "=", 2) 250 if len(stringSlice) > 1 { 251 // If there is a conflict we will return an error 252 if v, ok := labelMap[stringSlice[0]]; ok && v != stringSlice[1] { 253 return nil, fmt.Errorf("conflict labels for %s=%s and %s=%s", stringSlice[0], stringSlice[1], stringSlice[0], v) 254 } 255 labelMap[stringSlice[0]] = stringSlice[1] 256 } 257 } 258 259 newLabels := []string{} 260 for k, v := range labelMap { 261 newLabels = append(newLabels, fmt.Sprintf("%s=%s", k, v)) 262 } 263 return newLabels, nil 264 } 265 266 // ReloadConfiguration reads the configuration in the host and reloads the daemon and server. 267 func ReloadConfiguration(configFile string, flags *pflag.FlagSet, reload func(*Config)) error { 268 logrus.Infof("Got signal to reload configuration, reloading from: %s", configFile) 269 newConfig, err := getConflictFreeConfiguration(configFile, flags) 270 if err != nil { 271 return err 272 } 273 274 if err := ValidateConfiguration(newConfig); err != nil { 275 return fmt.Errorf("file configuration validation failed (%v)", err) 276 } 277 278 // Labels of the docker engine used to allow multiple values associated with the same key. 279 // This is deprecated in 1.13, and, be removed after 3 release cycles. 280 // The following will check the conflict of labels, and report a warning for deprecation. 281 // 282 // TODO: After 3 release cycles (1.16) an error will be returned, and labels will be 283 // sanitized to consolidate duplicate key-value pairs (config.Labels = newLabels): 284 // 285 // newLabels, err := GetConflictFreeLabels(newConfig.Labels) 286 // if err != nil { 287 // return err 288 // } 289 // newConfig.Labels = newLabels 290 // 291 if _, err := GetConflictFreeLabels(newConfig.Labels); err != nil { 292 logrus.Warnf("Engine labels with duplicate keys and conflicting values have been deprecated: %s", err) 293 } 294 295 reload(newConfig) 296 return nil 297 } 298 299 // boolValue is an interface that boolean value flags implement 300 // to tell the command line how to make -name equivalent to -name=true. 301 type boolValue interface { 302 IsBoolFlag() bool 303 } 304 305 // MergeDaemonConfigurations reads a configuration file, 306 // loads the file configuration in an isolated structure, 307 // and merges the configuration provided from flags on top 308 // if there are no conflicts. 309 func MergeDaemonConfigurations(flagsConfig *Config, flags *pflag.FlagSet, configFile string) (*Config, error) { 310 fileConfig, err := getConflictFreeConfiguration(configFile, flags) 311 if err != nil { 312 return nil, err 313 } 314 315 if err := ValidateConfiguration(fileConfig); err != nil { 316 return nil, fmt.Errorf("file configuration validation failed (%v)", err) 317 } 318 319 // merge flags configuration on top of the file configuration 320 if err := mergo.Merge(fileConfig, flagsConfig); err != nil { 321 return nil, err 322 } 323 324 // We need to validate again once both fileConfig and flagsConfig 325 // have been merged 326 if err := ValidateConfiguration(fileConfig); err != nil { 327 return nil, fmt.Errorf("file configuration validation failed (%v)", err) 328 } 329 330 return fileConfig, nil 331 } 332 333 // getConflictFreeConfiguration loads the configuration from a JSON file. 334 // It compares that configuration with the one provided by the flags, 335 // and returns an error if there are conflicts. 336 func getConflictFreeConfiguration(configFile string, flags *pflag.FlagSet) (*Config, error) { 337 b, err := ioutil.ReadFile(configFile) 338 if err != nil { 339 return nil, err 340 } 341 342 var config Config 343 var reader io.Reader 344 if flags != nil { 345 var jsonConfig map[string]interface{} 346 reader = bytes.NewReader(b) 347 if err := json.NewDecoder(reader).Decode(&jsonConfig); err != nil { 348 return nil, err 349 } 350 351 configSet := configValuesSet(jsonConfig) 352 353 if err := findConfigurationConflicts(configSet, flags); err != nil { 354 return nil, err 355 } 356 357 // Override flag values to make sure the values set in the config file with nullable values, like `false`, 358 // are not overridden by default truthy values from the flags that were not explicitly set. 359 // See https://github.com/docker/docker/issues/20289 for an example. 360 // 361 // TODO: Rewrite configuration logic to avoid same issue with other nullable values, like numbers. 362 namedOptions := make(map[string]interface{}) 363 for key, value := range configSet { 364 f := flags.Lookup(key) 365 if f == nil { // ignore named flags that don't match 366 namedOptions[key] = value 367 continue 368 } 369 370 if _, ok := f.Value.(boolValue); ok { 371 f.Value.Set(fmt.Sprintf("%v", value)) 372 } 373 } 374 if len(namedOptions) > 0 { 375 // set also default for mergeVal flags that are boolValue at the same time. 376 flags.VisitAll(func(f *pflag.Flag) { 377 if opt, named := f.Value.(opts.NamedOption); named { 378 v, set := namedOptions[opt.Name()] 379 _, boolean := f.Value.(boolValue) 380 if set && boolean { 381 f.Value.Set(fmt.Sprintf("%v", v)) 382 } 383 } 384 }) 385 } 386 387 config.valuesSet = configSet 388 } 389 390 reader = bytes.NewReader(b) 391 err = json.NewDecoder(reader).Decode(&config) 392 return &config, err 393 } 394 395 // configValuesSet returns the configuration values explicitly set in the file. 396 func configValuesSet(config map[string]interface{}) map[string]interface{} { 397 flatten := make(map[string]interface{}) 398 for k, v := range config { 399 if m, isMap := v.(map[string]interface{}); isMap && !flatOptions[k] { 400 for km, vm := range m { 401 flatten[km] = vm 402 } 403 continue 404 } 405 406 flatten[k] = v 407 } 408 return flatten 409 } 410 411 // findConfigurationConflicts iterates over the provided flags searching for 412 // duplicated configurations and unknown keys. It returns an error with all the conflicts if 413 // it finds any. 414 func findConfigurationConflicts(config map[string]interface{}, flags *pflag.FlagSet) error { 415 // 1. Search keys from the file that we don't recognize as flags. 416 unknownKeys := make(map[string]interface{}) 417 for key, value := range config { 418 if flag := flags.Lookup(key); flag == nil { 419 unknownKeys[key] = value 420 } 421 } 422 423 // 2. Discard values that implement NamedOption. 424 // Their configuration name differs from their flag name, like `labels` and `label`. 425 if len(unknownKeys) > 0 { 426 unknownNamedConflicts := func(f *pflag.Flag) { 427 if namedOption, ok := f.Value.(opts.NamedOption); ok { 428 if _, valid := unknownKeys[namedOption.Name()]; valid { 429 delete(unknownKeys, namedOption.Name()) 430 } 431 } 432 } 433 flags.VisitAll(unknownNamedConflicts) 434 } 435 436 if len(unknownKeys) > 0 { 437 var unknown []string 438 for key := range unknownKeys { 439 unknown = append(unknown, key) 440 } 441 return fmt.Errorf("the following directives don't match any configuration option: %s", strings.Join(unknown, ", ")) 442 } 443 444 var conflicts []string 445 printConflict := func(name string, flagValue, fileValue interface{}) string { 446 return fmt.Sprintf("%s: (from flag: %v, from file: %v)", name, flagValue, fileValue) 447 } 448 449 // 3. Search keys that are present as a flag and as a file option. 450 duplicatedConflicts := func(f *pflag.Flag) { 451 // search option name in the json configuration payload if the value is a named option 452 if namedOption, ok := f.Value.(opts.NamedOption); ok { 453 if optsValue, ok := config[namedOption.Name()]; ok { 454 conflicts = append(conflicts, printConflict(namedOption.Name(), f.Value.String(), optsValue)) 455 } 456 } else { 457 // search flag name in the json configuration payload 458 for _, name := range []string{f.Name, f.Shorthand} { 459 if value, ok := config[name]; ok { 460 conflicts = append(conflicts, printConflict(name, f.Value.String(), value)) 461 break 462 } 463 } 464 } 465 } 466 467 flags.Visit(duplicatedConflicts) 468 469 if len(conflicts) > 0 { 470 return fmt.Errorf("the following directives are specified both as a flag and in the configuration file: %s", strings.Join(conflicts, ", ")) 471 } 472 return nil 473 } 474 475 // ValidateConfiguration validates some specific configs. 476 // such as config.DNS, config.Labels, config.DNSSearch, 477 // as well as config.MaxConcurrentDownloads, config.MaxConcurrentUploads. 478 func ValidateConfiguration(config *Config) error { 479 // validate DNS 480 for _, dns := range config.DNS { 481 if _, err := opts.ValidateIPAddress(dns); err != nil { 482 return err 483 } 484 } 485 486 // validate DNSSearch 487 for _, dnsSearch := range config.DNSSearch { 488 if _, err := opts.ValidateDNSSearch(dnsSearch); err != nil { 489 return err 490 } 491 } 492 493 // validate Labels 494 for _, label := range config.Labels { 495 if _, err := opts.ValidateLabel(label); err != nil { 496 return err 497 } 498 } 499 500 // validate MaxConcurrentDownloads 501 if config.IsValueSet("max-concurrent-downloads") && config.MaxConcurrentDownloads != nil && *config.MaxConcurrentDownloads < 0 { 502 return fmt.Errorf("invalid max concurrent downloads: %d", *config.MaxConcurrentDownloads) 503 } 504 505 // validate MaxConcurrentUploads 506 if config.IsValueSet("max-concurrent-uploads") && config.MaxConcurrentUploads != nil && *config.MaxConcurrentUploads < 0 { 507 return fmt.Errorf("invalid max concurrent uploads: %d", *config.MaxConcurrentUploads) 508 } 509 510 // validate that "default" runtime is not reset 511 if runtimes := config.GetAllRuntimes(); len(runtimes) > 0 { 512 if _, ok := runtimes[stockRuntimeName]; ok { 513 return fmt.Errorf("runtime name '%s' is reserved", stockRuntimeName) 514 } 515 } 516 517 if defaultRuntime := config.GetDefaultRuntimeName(); defaultRuntime != "" && defaultRuntime != stockRuntimeName { 518 runtimes := config.GetAllRuntimes() 519 if _, ok := runtimes[defaultRuntime]; !ok { 520 return fmt.Errorf("specified default runtime '%s' does not exist", defaultRuntime) 521 } 522 } 523 524 return nil 525 }