github.com/jfrazelle/docker@v1.1.2-0.20210712172922-bf78e25fe508/daemon/config/config.go (about) 1 package config // import "github.com/docker/docker/daemon/config" 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "net" 9 "os" 10 "reflect" 11 "strings" 12 "sync" 13 14 daemondiscovery "github.com/docker/docker/daemon/discovery" 15 "github.com/docker/docker/opts" 16 "github.com/docker/docker/pkg/authorization" 17 "github.com/docker/docker/pkg/discovery" 18 "github.com/docker/docker/registry" 19 "github.com/imdario/mergo" 20 "github.com/pkg/errors" 21 "github.com/sirupsen/logrus" 22 "github.com/spf13/pflag" 23 ) 24 25 const ( 26 // DefaultMaxConcurrentDownloads is the default value for 27 // maximum number of downloads that 28 // may take place at a time for each pull. 29 DefaultMaxConcurrentDownloads = 3 30 // DefaultMaxConcurrentUploads is the default value for 31 // maximum number of uploads that 32 // may take place at a time for each push. 33 DefaultMaxConcurrentUploads = 5 34 // DefaultDownloadAttempts is the default value for 35 // maximum number of attempts that 36 // may take place at a time for each pull when the connection is lost. 37 DefaultDownloadAttempts = 5 38 // DefaultShmSize is the default value for container's shm size 39 DefaultShmSize = int64(67108864) 40 // DefaultNetworkMtu is the default value for network MTU 41 DefaultNetworkMtu = 1500 42 // DisableNetworkBridge is the default value of the option to disable network bridge 43 DisableNetworkBridge = "none" 44 // DefaultInitBinary is the name of the default init binary 45 DefaultInitBinary = "docker-init" 46 // DefaultShimBinary is the default shim to be used by containerd if none 47 // is specified 48 DefaultShimBinary = "containerd-shim" 49 // DefaultRuntimeBinary is the default runtime to be used by 50 // containerd if none is specified 51 DefaultRuntimeBinary = "runc" 52 // StockRuntimeName is the reserved name/alias used to represent the 53 // OCI runtime being shipped with the docker daemon package. 54 StockRuntimeName = "runc" 55 // LinuxV1RuntimeName is the runtime used to specify the containerd v1 shim with the runc binary 56 // Note this is different than io.containerd.runc.v1 which would be the v1 shim using the v2 shim API. 57 // This is specifically for the v1 shim using the v1 shim API. 58 LinuxV1RuntimeName = "io.containerd.runtime.v1.linux" 59 // LinuxV2RuntimeName is the runtime used to specify the containerd v2 runc shim 60 LinuxV2RuntimeName = "io.containerd.runc.v2" 61 ) 62 63 var builtinRuntimes = map[string]bool{ 64 StockRuntimeName: true, 65 LinuxV1RuntimeName: true, 66 LinuxV2RuntimeName: true, 67 } 68 69 // flatOptions contains configuration keys 70 // that MUST NOT be parsed as deep structures. 71 // Use this to differentiate these options 72 // with others like the ones in CommonTLSOptions. 73 var flatOptions = map[string]bool{ 74 "cluster-store-opts": true, 75 "log-opts": true, 76 "runtimes": true, 77 "default-ulimits": true, 78 "features": true, 79 "builder": true, 80 } 81 82 // skipValidateOptions contains configuration keys 83 // that will be skipped from findConfigurationConflicts 84 // for unknown flag validation. 85 var skipValidateOptions = map[string]bool{ 86 "features": true, 87 "builder": true, 88 // Corresponding flag has been removed because it was already unusable 89 "deprecated-key-path": true, 90 } 91 92 // skipDuplicates contains configuration keys that 93 // will be skipped when checking duplicated 94 // configuration field defined in both daemon 95 // config file and from dockerd cli flags. 96 // This allows some configurations to be merged 97 // during the parsing. 98 var skipDuplicates = map[string]bool{ 99 "runtimes": true, 100 } 101 102 // LogConfig represents the default log configuration. 103 // It includes json tags to deserialize configuration from a file 104 // using the same names that the flags in the command line use. 105 type LogConfig struct { 106 Type string `json:"log-driver,omitempty"` 107 Config map[string]string `json:"log-opts,omitempty"` 108 } 109 110 // commonBridgeConfig stores all the platform-common bridge driver specific 111 // configuration. 112 type commonBridgeConfig struct { 113 Iface string `json:"bridge,omitempty"` 114 FixedCIDR string `json:"fixed-cidr,omitempty"` 115 } 116 117 // NetworkConfig stores the daemon-wide networking configurations 118 type NetworkConfig struct { 119 // Default address pools for docker networks 120 DefaultAddressPools opts.PoolsOpt `json:"default-address-pools,omitempty"` 121 // NetworkControlPlaneMTU allows to specify the control plane MTU, this will allow to optimize the network use in some components 122 NetworkControlPlaneMTU int `json:"network-control-plane-mtu,omitempty"` 123 } 124 125 // CommonTLSOptions defines TLS configuration for the daemon server. 126 // It includes json tags to deserialize configuration from a file 127 // using the same names that the flags in the command line use. 128 type CommonTLSOptions struct { 129 CAFile string `json:"tlscacert,omitempty"` 130 CertFile string `json:"tlscert,omitempty"` 131 KeyFile string `json:"tlskey,omitempty"` 132 } 133 134 // DNSConfig defines the DNS configurations. 135 type DNSConfig struct { 136 DNS []string `json:"dns,omitempty"` 137 DNSOptions []string `json:"dns-opts,omitempty"` 138 DNSSearch []string `json:"dns-search,omitempty"` 139 HostGatewayIP net.IP `json:"host-gateway-ip,omitempty"` 140 } 141 142 // CommonConfig defines the configuration of a docker daemon which is 143 // common across platforms. 144 // It includes json tags to deserialize configuration from a file 145 // using the same names that the flags in the command line use. 146 type CommonConfig struct { 147 AuthzMiddleware *authorization.Middleware `json:"-"` 148 AuthorizationPlugins []string `json:"authorization-plugins,omitempty"` // AuthorizationPlugins holds list of authorization plugins 149 AutoRestart bool `json:"-"` 150 Context map[string][]string `json:"-"` 151 DisableBridge bool `json:"-"` 152 ExecOptions []string `json:"exec-opts,omitempty"` 153 GraphDriver string `json:"storage-driver,omitempty"` 154 GraphOptions []string `json:"storage-opts,omitempty"` 155 Labels []string `json:"labels,omitempty"` 156 Mtu int `json:"mtu,omitempty"` 157 NetworkDiagnosticPort int `json:"network-diagnostic-port,omitempty"` 158 Pidfile string `json:"pidfile,omitempty"` 159 RawLogs bool `json:"raw-logs,omitempty"` 160 RootDeprecated string `json:"graph,omitempty"` 161 Root string `json:"data-root,omitempty"` 162 ExecRoot string `json:"exec-root,omitempty"` 163 SocketGroup string `json:"group,omitempty"` 164 CorsHeaders string `json:"api-cors-header,omitempty"` 165 166 // TrustKeyPath is used to generate the daemon ID and for signing schema 1 manifests 167 // when pushing to a registry which does not support schema 2. This field is marked as 168 // deprecated because schema 1 manifests are deprecated in favor of schema 2 and the 169 // daemon ID will use a dedicated identifier not shared with exported signatures. 170 TrustKeyPath string `json:"deprecated-key-path,omitempty"` 171 172 // LiveRestoreEnabled determines whether we should keep containers 173 // alive upon daemon shutdown/start 174 LiveRestoreEnabled bool `json:"live-restore,omitempty"` 175 176 // ClusterStore is the storage backend used for the cluster information. It is used by both 177 // multihost networking (to store networks and endpoints information) and by the node discovery 178 // mechanism. 179 // Deprecated: host-discovery and overlay networks with external k/v stores are deprecated 180 ClusterStore string `json:"cluster-store,omitempty"` 181 182 // ClusterOpts is used to pass options to the discovery package for tuning libkv settings, such 183 // as TLS configuration settings. 184 // Deprecated: host-discovery and overlay networks with external k/v stores are deprecated 185 ClusterOpts map[string]string `json:"cluster-store-opts,omitempty"` 186 187 // ClusterAdvertise is the network endpoint that the Engine advertises for the purpose of node 188 // discovery. This should be a 'host:port' combination on which that daemon instance is 189 // reachable by other hosts. 190 // Deprecated: host-discovery and overlay networks with external k/v stores are deprecated 191 ClusterAdvertise string `json:"cluster-advertise,omitempty"` 192 193 // MaxConcurrentDownloads is the maximum number of downloads that 194 // may take place at a time for each pull. 195 MaxConcurrentDownloads *int `json:"max-concurrent-downloads,omitempty"` 196 197 // MaxConcurrentUploads is the maximum number of uploads that 198 // may take place at a time for each push. 199 MaxConcurrentUploads *int `json:"max-concurrent-uploads,omitempty"` 200 201 // MaxDownloadAttempts is the maximum number of attempts that 202 // may take place at a time for each push. 203 MaxDownloadAttempts *int `json:"max-download-attempts,omitempty"` 204 205 // ShutdownTimeout is the timeout value (in seconds) the daemon will wait for the container 206 // to stop when daemon is being shutdown 207 ShutdownTimeout int `json:"shutdown-timeout,omitempty"` 208 209 Debug bool `json:"debug,omitempty"` 210 Hosts []string `json:"hosts,omitempty"` 211 LogLevel string `json:"log-level,omitempty"` 212 TLS *bool `json:"tls,omitempty"` 213 TLSVerify *bool `json:"tlsverify,omitempty"` 214 215 // Embedded structs that allow config 216 // deserialization without the full struct. 217 CommonTLSOptions 218 219 // SwarmDefaultAdvertiseAddr is the default host/IP or network interface 220 // to use if a wildcard address is specified in the ListenAddr value 221 // given to the /swarm/init endpoint and no advertise address is 222 // specified. 223 SwarmDefaultAdvertiseAddr string `json:"swarm-default-advertise-addr"` 224 225 // SwarmRaftHeartbeatTick is the number of ticks in time for swarm mode raft quorum heartbeat 226 // Typical value is 1 227 SwarmRaftHeartbeatTick uint32 `json:"swarm-raft-heartbeat-tick"` 228 229 // SwarmRaftElectionTick is the number of ticks to elapse before followers in the quorum can propose 230 // a new round of leader election. Default, recommended value is at least 10X that of Heartbeat tick. 231 // Higher values can make the quorum less sensitive to transient faults in the environment, but this also 232 // means it takes longer for the managers to detect a down leader. 233 SwarmRaftElectionTick uint32 `json:"swarm-raft-election-tick"` 234 235 MetricsAddress string `json:"metrics-addr"` 236 237 DNSConfig 238 LogConfig 239 BridgeConfig // bridgeConfig holds bridge network specific configuration. 240 NetworkConfig 241 registry.ServiceOptions 242 243 sync.Mutex 244 // FIXME(vdemeester) This part is not that clear and is mainly dependent on cli flags 245 // It should probably be handled outside this package. 246 ValuesSet map[string]interface{} `json:"-"` 247 248 Experimental bool `json:"experimental"` // Experimental indicates whether experimental features should be exposed or not 249 250 // Exposed node Generic Resources 251 // e.g: ["orange=red", "orange=green", "orange=blue", "apple=3"] 252 NodeGenericResources []string `json:"node-generic-resources,omitempty"` 253 254 // ContainerAddr is the address used to connect to containerd if we're 255 // not starting it ourselves 256 ContainerdAddr string `json:"containerd,omitempty"` 257 258 // CriContainerd determines whether a supervised containerd instance 259 // should be configured with the CRI plugin enabled. This allows using 260 // Docker's containerd instance directly with a Kubernetes kubelet. 261 CriContainerd bool `json:"cri-containerd,omitempty"` 262 263 // Features contains a list of feature key value pairs indicating what features are enabled or disabled. 264 // If a certain feature doesn't appear in this list then it's unset (i.e. neither true nor false). 265 Features map[string]bool `json:"features,omitempty"` 266 267 Builder BuilderConfig `json:"builder,omitempty"` 268 269 ContainerdNamespace string `json:"containerd-namespace,omitempty"` 270 ContainerdPluginNamespace string `json:"containerd-plugin-namespace,omitempty"` 271 } 272 273 // IsValueSet returns true if a configuration value 274 // was explicitly set in the configuration file. 275 func (conf *Config) IsValueSet(name string) bool { 276 if conf.ValuesSet == nil { 277 return false 278 } 279 _, ok := conf.ValuesSet[name] 280 return ok 281 } 282 283 // New returns a new fully initialized Config struct 284 func New() *Config { 285 config := Config{} 286 config.LogConfig.Config = make(map[string]string) 287 config.ClusterOpts = make(map[string]string) 288 return &config 289 } 290 291 // ParseClusterAdvertiseSettings parses the specified advertise settings 292 func ParseClusterAdvertiseSettings(clusterStore, clusterAdvertise string) (string, error) { 293 if clusterAdvertise == "" { 294 return "", daemondiscovery.ErrDiscoveryDisabled 295 } 296 if clusterStore == "" { 297 return "", errors.New("invalid cluster configuration. --cluster-advertise must be accompanied by --cluster-store configuration") 298 } 299 300 advertise, err := discovery.ParseAdvertise(clusterAdvertise) 301 if err != nil { 302 return "", errors.Wrap(err, "discovery advertise parsing failed") 303 } 304 return advertise, nil 305 } 306 307 // GetConflictFreeLabels validates Labels for conflict 308 // In swarm the duplicates for labels are removed 309 // so we only take same values here, no conflict values 310 // If the key-value is the same we will only take the last label 311 func GetConflictFreeLabels(labels []string) ([]string, error) { 312 labelMap := map[string]string{} 313 for _, label := range labels { 314 stringSlice := strings.SplitN(label, "=", 2) 315 if len(stringSlice) > 1 { 316 // If there is a conflict we will return an error 317 if v, ok := labelMap[stringSlice[0]]; ok && v != stringSlice[1] { 318 return nil, fmt.Errorf("conflict labels for %s=%s and %s=%s", stringSlice[0], stringSlice[1], stringSlice[0], v) 319 } 320 labelMap[stringSlice[0]] = stringSlice[1] 321 } 322 } 323 324 newLabels := []string{} 325 for k, v := range labelMap { 326 newLabels = append(newLabels, fmt.Sprintf("%s=%s", k, v)) 327 } 328 return newLabels, nil 329 } 330 331 // Reload reads the configuration in the host and reloads the daemon and server. 332 func Reload(configFile string, flags *pflag.FlagSet, reload func(*Config)) error { 333 logrus.Infof("Got signal to reload configuration, reloading from: %s", configFile) 334 newConfig, err := getConflictFreeConfiguration(configFile, flags) 335 if err != nil { 336 if flags.Changed("config-file") || !os.IsNotExist(err) { 337 return errors.Wrapf(err, "unable to configure the Docker daemon with file %s", configFile) 338 } 339 newConfig = New() 340 } 341 342 if err := Validate(newConfig); err != nil { 343 return errors.Wrap(err, "file configuration validation failed") 344 } 345 346 // Check if duplicate label-keys with different values are found 347 newLabels, err := GetConflictFreeLabels(newConfig.Labels) 348 if err != nil { 349 return err 350 } 351 newConfig.Labels = newLabels 352 353 reload(newConfig) 354 return nil 355 } 356 357 // boolValue is an interface that boolean value flags implement 358 // to tell the command line how to make -name equivalent to -name=true. 359 type boolValue interface { 360 IsBoolFlag() bool 361 } 362 363 // MergeDaemonConfigurations reads a configuration file, 364 // loads the file configuration in an isolated structure, 365 // and merges the configuration provided from flags on top 366 // if there are no conflicts. 367 func MergeDaemonConfigurations(flagsConfig *Config, flags *pflag.FlagSet, configFile string) (*Config, error) { 368 fileConfig, err := getConflictFreeConfiguration(configFile, flags) 369 if err != nil { 370 return nil, err 371 } 372 373 if err := Validate(fileConfig); err != nil { 374 return nil, errors.Wrap(err, "configuration validation from file failed") 375 } 376 377 // merge flags configuration on top of the file configuration 378 if err := mergo.Merge(fileConfig, flagsConfig); err != nil { 379 return nil, err 380 } 381 382 // We need to validate again once both fileConfig and flagsConfig 383 // have been merged 384 if err := Validate(fileConfig); err != nil { 385 return nil, errors.Wrap(err, "merged configuration validation from file and command line flags failed") 386 } 387 388 return fileConfig, nil 389 } 390 391 // getConflictFreeConfiguration loads the configuration from a JSON file. 392 // It compares that configuration with the one provided by the flags, 393 // and returns an error if there are conflicts. 394 func getConflictFreeConfiguration(configFile string, flags *pflag.FlagSet) (*Config, error) { 395 b, err := ioutil.ReadFile(configFile) 396 if err != nil { 397 return nil, err 398 } 399 400 var config Config 401 402 b = bytes.TrimSpace(b) 403 if len(b) == 0 { 404 // empty config file 405 return &config, nil 406 } 407 408 if flags != nil { 409 var jsonConfig map[string]interface{} 410 if err := json.Unmarshal(b, &jsonConfig); err != nil { 411 return nil, err 412 } 413 414 configSet := configValuesSet(jsonConfig) 415 416 if err := findConfigurationConflicts(configSet, flags); err != nil { 417 return nil, err 418 } 419 420 // Override flag values to make sure the values set in the config file with nullable values, like `false`, 421 // are not overridden by default truthy values from the flags that were not explicitly set. 422 // See https://github.com/docker/docker/issues/20289 for an example. 423 // 424 // TODO: Rewrite configuration logic to avoid same issue with other nullable values, like numbers. 425 namedOptions := make(map[string]interface{}) 426 for key, value := range configSet { 427 f := flags.Lookup(key) 428 if f == nil { // ignore named flags that don't match 429 namedOptions[key] = value 430 continue 431 } 432 433 if _, ok := f.Value.(boolValue); ok { 434 f.Value.Set(fmt.Sprintf("%v", value)) 435 } 436 } 437 if len(namedOptions) > 0 { 438 // set also default for mergeVal flags that are boolValue at the same time. 439 flags.VisitAll(func(f *pflag.Flag) { 440 if opt, named := f.Value.(opts.NamedOption); named { 441 v, set := namedOptions[opt.Name()] 442 _, boolean := f.Value.(boolValue) 443 if set && boolean { 444 f.Value.Set(fmt.Sprintf("%v", v)) 445 } 446 } 447 }) 448 } 449 450 config.ValuesSet = configSet 451 } 452 453 if err := json.Unmarshal(b, &config); err != nil { 454 return nil, err 455 } 456 457 if config.RootDeprecated != "" { 458 logrus.Warn(`The "graph" config file option is deprecated. Please use "data-root" instead.`) 459 460 if config.Root != "" { 461 return nil, errors.New(`cannot specify both "graph" and "data-root" config file options`) 462 } 463 464 config.Root = config.RootDeprecated 465 } 466 467 return &config, nil 468 } 469 470 // configValuesSet returns the configuration values explicitly set in the file. 471 func configValuesSet(config map[string]interface{}) map[string]interface{} { 472 flatten := make(map[string]interface{}) 473 for k, v := range config { 474 if m, isMap := v.(map[string]interface{}); isMap && !flatOptions[k] { 475 for km, vm := range m { 476 flatten[km] = vm 477 } 478 continue 479 } 480 481 flatten[k] = v 482 } 483 return flatten 484 } 485 486 // findConfigurationConflicts iterates over the provided flags searching for 487 // duplicated configurations and unknown keys. It returns an error with all the conflicts if 488 // it finds any. 489 func findConfigurationConflicts(config map[string]interface{}, flags *pflag.FlagSet) error { 490 // 1. Search keys from the file that we don't recognize as flags. 491 unknownKeys := make(map[string]interface{}) 492 for key, value := range config { 493 if flag := flags.Lookup(key); flag == nil && !skipValidateOptions[key] { 494 unknownKeys[key] = value 495 } 496 } 497 498 // 2. Discard values that implement NamedOption. 499 // Their configuration name differs from their flag name, like `labels` and `label`. 500 if len(unknownKeys) > 0 { 501 unknownNamedConflicts := func(f *pflag.Flag) { 502 if namedOption, ok := f.Value.(opts.NamedOption); ok { 503 delete(unknownKeys, namedOption.Name()) 504 } 505 } 506 flags.VisitAll(unknownNamedConflicts) 507 } 508 509 if len(unknownKeys) > 0 { 510 var unknown []string 511 for key := range unknownKeys { 512 unknown = append(unknown, key) 513 } 514 return fmt.Errorf("the following directives don't match any configuration option: %s", strings.Join(unknown, ", ")) 515 } 516 517 var conflicts []string 518 printConflict := func(name string, flagValue, fileValue interface{}) string { 519 return fmt.Sprintf("%s: (from flag: %v, from file: %v)", name, flagValue, fileValue) 520 } 521 522 // 3. Search keys that are present as a flag and as a file option. 523 duplicatedConflicts := func(f *pflag.Flag) { 524 // search option name in the json configuration payload if the value is a named option 525 if namedOption, ok := f.Value.(opts.NamedOption); ok { 526 if optsValue, ok := config[namedOption.Name()]; ok && !skipDuplicates[namedOption.Name()] { 527 conflicts = append(conflicts, printConflict(namedOption.Name(), f.Value.String(), optsValue)) 528 } 529 } else { 530 // search flag name in the json configuration payload 531 for _, name := range []string{f.Name, f.Shorthand} { 532 if value, ok := config[name]; ok && !skipDuplicates[name] { 533 conflicts = append(conflicts, printConflict(name, f.Value.String(), value)) 534 break 535 } 536 } 537 } 538 } 539 540 flags.Visit(duplicatedConflicts) 541 542 if len(conflicts) > 0 { 543 return fmt.Errorf("the following directives are specified both as a flag and in the configuration file: %s", strings.Join(conflicts, ", ")) 544 } 545 return nil 546 } 547 548 // Validate validates some specific configs. 549 // such as config.DNS, config.Labels, config.DNSSearch, 550 // as well as config.MaxConcurrentDownloads, config.MaxConcurrentUploads and config.MaxDownloadAttempts. 551 func Validate(config *Config) error { 552 // validate DNS 553 for _, dns := range config.DNS { 554 if _, err := opts.ValidateIPAddress(dns); err != nil { 555 return err 556 } 557 } 558 559 // validate DNSSearch 560 for _, dnsSearch := range config.DNSSearch { 561 if _, err := opts.ValidateDNSSearch(dnsSearch); err != nil { 562 return err 563 } 564 } 565 566 // validate Labels 567 for _, label := range config.Labels { 568 if _, err := opts.ValidateLabel(label); err != nil { 569 return err 570 } 571 } 572 // validate MaxConcurrentDownloads 573 if config.MaxConcurrentDownloads != nil && *config.MaxConcurrentDownloads < 0 { 574 return fmt.Errorf("invalid max concurrent downloads: %d", *config.MaxConcurrentDownloads) 575 } 576 // validate MaxConcurrentUploads 577 if config.MaxConcurrentUploads != nil && *config.MaxConcurrentUploads < 0 { 578 return fmt.Errorf("invalid max concurrent uploads: %d", *config.MaxConcurrentUploads) 579 } 580 if err := ValidateMaxDownloadAttempts(config); err != nil { 581 return err 582 } 583 584 // validate that "default" runtime is not reset 585 if runtimes := config.GetAllRuntimes(); len(runtimes) > 0 { 586 if _, ok := runtimes[StockRuntimeName]; ok { 587 return fmt.Errorf("runtime name '%s' is reserved", StockRuntimeName) 588 } 589 } 590 591 if _, err := ParseGenericResources(config.NodeGenericResources); err != nil { 592 return err 593 } 594 595 if defaultRuntime := config.GetDefaultRuntimeName(); defaultRuntime != "" { 596 if !builtinRuntimes[defaultRuntime] { 597 runtimes := config.GetAllRuntimes() 598 if _, ok := runtimes[defaultRuntime]; !ok { 599 return fmt.Errorf("specified default runtime '%s' does not exist", defaultRuntime) 600 } 601 } 602 } 603 604 // validate platform-specific settings 605 return config.ValidatePlatformConfig() 606 } 607 608 // ValidateMaxDownloadAttempts validates if the max-download-attempts is within the valid range 609 func ValidateMaxDownloadAttempts(config *Config) error { 610 if config.MaxDownloadAttempts != nil && *config.MaxDownloadAttempts <= 0 { 611 return fmt.Errorf("invalid max download attempts: %d", *config.MaxDownloadAttempts) 612 } 613 return nil 614 } 615 616 // ModifiedDiscoverySettings returns whether the discovery configuration has been modified or not. 617 func ModifiedDiscoverySettings(config *Config, backendType, advertise string, clusterOpts map[string]string) bool { 618 if config.ClusterStore != backendType || config.ClusterAdvertise != advertise { 619 return true 620 } 621 622 if (config.ClusterOpts == nil && clusterOpts == nil) || 623 (config.ClusterOpts == nil && len(clusterOpts) == 0) || 624 (len(config.ClusterOpts) == 0 && clusterOpts == nil) { 625 return false 626 } 627 628 return !reflect.DeepEqual(config.ClusterOpts, clusterOpts) 629 }