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