github.com/Heebron/moby@v0.0.0-20221111184709-6eab4f55faf7/daemon/config/config_test.go (about) 1 package config // import "github.com/docker/docker/daemon/config" 2 3 import ( 4 "os" 5 "reflect" 6 "strings" 7 "testing" 8 9 "github.com/docker/docker/libnetwork/ipamutils" 10 "github.com/docker/docker/opts" 11 "github.com/google/go-cmp/cmp" 12 "github.com/google/go-cmp/cmp/cmpopts" 13 "github.com/imdario/mergo" 14 "github.com/spf13/pflag" 15 "gotest.tools/v3/assert" 16 is "gotest.tools/v3/assert/cmp" 17 "gotest.tools/v3/fs" 18 "gotest.tools/v3/skip" 19 ) 20 21 func TestDaemonConfigurationNotFound(t *testing.T) { 22 _, err := MergeDaemonConfigurations(&Config{}, nil, "/tmp/foo-bar-baz-docker") 23 assert.Check(t, os.IsNotExist(err), "got: %[1]T: %[1]v", err) 24 } 25 26 func TestDaemonBrokenConfiguration(t *testing.T) { 27 f, err := os.CreateTemp("", "docker-config-") 28 assert.NilError(t, err) 29 30 configFile := f.Name() 31 f.Write([]byte(`{"Debug": tru`)) 32 f.Close() 33 34 _, err = MergeDaemonConfigurations(&Config{}, nil, configFile) 35 assert.ErrorContains(t, err, `invalid character ' ' in literal true`) 36 } 37 38 func TestFindConfigurationConflicts(t *testing.T) { 39 config := map[string]interface{}{"authorization-plugins": "foobar"} 40 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 41 42 flags.String("authorization-plugins", "", "") 43 assert.Check(t, flags.Set("authorization-plugins", "asdf")) 44 assert.Check(t, is.ErrorContains(findConfigurationConflicts(config, flags), "authorization-plugins: (from flag: asdf, from file: foobar)")) 45 } 46 47 func TestFindConfigurationConflictsWithNamedOptions(t *testing.T) { 48 config := map[string]interface{}{"hosts": []string{"qwer"}} 49 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 50 51 var hosts []string 52 flags.VarP(opts.NewNamedListOptsRef("hosts", &hosts, opts.ValidateHost), "host", "H", "Daemon socket(s) to connect to") 53 assert.Check(t, flags.Set("host", "tcp://127.0.0.1:4444")) 54 assert.Check(t, flags.Set("host", "unix:///var/run/docker.sock")) 55 assert.Check(t, is.ErrorContains(findConfigurationConflicts(config, flags), "hosts")) 56 } 57 58 func TestDaemonConfigurationMergeConflicts(t *testing.T) { 59 f, err := os.CreateTemp("", "docker-config-") 60 assert.NilError(t, err) 61 62 configFile := f.Name() 63 f.Write([]byte(`{"debug": true}`)) 64 f.Close() 65 66 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 67 flags.Bool("debug", false, "") 68 assert.Check(t, flags.Set("debug", "false")) 69 70 _, err = MergeDaemonConfigurations(&Config{}, flags, configFile) 71 if err == nil { 72 t.Fatal("expected error, got nil") 73 } 74 if !strings.Contains(err.Error(), "debug") { 75 t.Fatalf("expected debug conflict, got %v", err) 76 } 77 } 78 79 func TestDaemonConfigurationMergeConcurrent(t *testing.T) { 80 f, err := os.CreateTemp("", "docker-config-") 81 assert.NilError(t, err) 82 83 configFile := f.Name() 84 f.Write([]byte(`{"max-concurrent-downloads": 1}`)) 85 f.Close() 86 87 _, err = MergeDaemonConfigurations(&Config{}, nil, configFile) 88 assert.NilError(t, err) 89 } 90 91 func TestDaemonConfigurationMergeConcurrentError(t *testing.T) { 92 f, err := os.CreateTemp("", "docker-config-") 93 assert.NilError(t, err) 94 95 configFile := f.Name() 96 f.Write([]byte(`{"max-concurrent-downloads": -1}`)) 97 f.Close() 98 99 _, err = MergeDaemonConfigurations(&Config{}, nil, configFile) 100 assert.ErrorContains(t, err, `invalid max concurrent downloads: -1`) 101 } 102 103 func TestDaemonConfigurationMergeConflictsWithInnerStructs(t *testing.T) { 104 f, err := os.CreateTemp("", "docker-config-") 105 assert.NilError(t, err) 106 107 configFile := f.Name() 108 f.Write([]byte(`{"tlscacert": "/etc/certificates/ca.pem"}`)) 109 f.Close() 110 111 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 112 flags.String("tlscacert", "", "") 113 assert.Check(t, flags.Set("tlscacert", "~/.docker/ca.pem")) 114 115 _, err = MergeDaemonConfigurations(&Config{}, flags, configFile) 116 assert.ErrorContains(t, err, `the following directives are specified both as a flag and in the configuration file: tlscacert`) 117 } 118 119 // Test for #40711 120 func TestDaemonConfigurationMergeDefaultAddressPools(t *testing.T) { 121 emptyConfigFile := fs.NewFile(t, "config", fs.WithContent(`{}`)) 122 defer emptyConfigFile.Remove() 123 configFile := fs.NewFile(t, "config", fs.WithContent(`{"default-address-pools":[{"base": "10.123.0.0/16", "size": 24 }]}`)) 124 defer configFile.Remove() 125 126 expected := []*ipamutils.NetworkToSplit{{Base: "10.123.0.0/16", Size: 24}} 127 128 t.Run("empty config file", func(t *testing.T) { 129 var conf = Config{} 130 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 131 flags.Var(&conf.NetworkConfig.DefaultAddressPools, "default-address-pool", "") 132 assert.Check(t, flags.Set("default-address-pool", "base=10.123.0.0/16,size=24")) 133 134 config, err := MergeDaemonConfigurations(&conf, flags, emptyConfigFile.Path()) 135 assert.NilError(t, err) 136 assert.DeepEqual(t, config.DefaultAddressPools.Value(), expected) 137 }) 138 139 t.Run("config file", func(t *testing.T) { 140 var conf = Config{} 141 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 142 flags.Var(&conf.NetworkConfig.DefaultAddressPools, "default-address-pool", "") 143 144 config, err := MergeDaemonConfigurations(&conf, flags, configFile.Path()) 145 assert.NilError(t, err) 146 assert.DeepEqual(t, config.DefaultAddressPools.Value(), expected) 147 }) 148 149 t.Run("with conflicting options", func(t *testing.T) { 150 var conf = Config{} 151 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 152 flags.Var(&conf.NetworkConfig.DefaultAddressPools, "default-address-pool", "") 153 assert.Check(t, flags.Set("default-address-pool", "base=10.123.0.0/16,size=24")) 154 155 _, err := MergeDaemonConfigurations(&conf, flags, configFile.Path()) 156 assert.ErrorContains(t, err, "the following directives are specified both as a flag and in the configuration file") 157 assert.ErrorContains(t, err, "default-address-pools") 158 }) 159 } 160 161 func TestFindConfigurationConflictsWithUnknownKeys(t *testing.T) { 162 config := map[string]interface{}{"tls-verify": "true"} 163 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 164 165 flags.Bool("tlsverify", false, "") 166 err := findConfigurationConflicts(config, flags) 167 assert.ErrorContains(t, err, "the following directives don't match any configuration option: tls-verify") 168 } 169 170 func TestFindConfigurationConflictsWithMergedValues(t *testing.T) { 171 var hosts []string 172 config := map[string]interface{}{"hosts": "tcp://127.0.0.1:2345"} 173 flags := pflag.NewFlagSet("base", pflag.ContinueOnError) 174 flags.VarP(opts.NewNamedListOptsRef("hosts", &hosts, nil), "host", "H", "") 175 176 err := findConfigurationConflicts(config, flags) 177 assert.NilError(t, err) 178 179 assert.Check(t, flags.Set("host", "unix:///var/run/docker.sock")) 180 err = findConfigurationConflicts(config, flags) 181 assert.ErrorContains(t, err, "hosts: (from flag: [unix:///var/run/docker.sock], from file: tcp://127.0.0.1:2345)") 182 } 183 184 func TestValidateConfigurationErrors(t *testing.T) { 185 testCases := []struct { 186 name string 187 field string 188 config *Config 189 expectedErr string 190 }{ 191 { 192 name: "single label without value", 193 config: &Config{ 194 CommonConfig: CommonConfig{ 195 Labels: []string{"one"}, 196 }, 197 }, 198 expectedErr: "bad attribute format: one", 199 }, 200 { 201 name: "multiple label without value", 202 config: &Config{ 203 CommonConfig: CommonConfig{ 204 Labels: []string{"foo=bar", "one"}, 205 }, 206 }, 207 expectedErr: "bad attribute format: one", 208 }, 209 { 210 name: "single DNS, invalid IP-address", 211 config: &Config{ 212 CommonConfig: CommonConfig{ 213 DNSConfig: DNSConfig{ 214 DNS: []string{"1.1.1.1o"}, 215 }, 216 }, 217 }, 218 expectedErr: "1.1.1.1o is not an ip address", 219 }, 220 { 221 name: "multiple DNS, invalid IP-address", 222 config: &Config{ 223 CommonConfig: CommonConfig{ 224 DNSConfig: DNSConfig{ 225 DNS: []string{"2.2.2.2", "1.1.1.1o"}, 226 }, 227 }, 228 }, 229 expectedErr: "1.1.1.1o is not an ip address", 230 }, 231 { 232 name: "single DNSSearch", 233 config: &Config{ 234 CommonConfig: CommonConfig{ 235 DNSConfig: DNSConfig{ 236 DNSSearch: []string{"123456"}, 237 }, 238 }, 239 }, 240 expectedErr: "123456 is not a valid domain", 241 }, 242 { 243 name: "multiple DNSSearch", 244 config: &Config{ 245 CommonConfig: CommonConfig{ 246 DNSConfig: DNSConfig{ 247 DNSSearch: []string{"a.b.c", "123456"}, 248 }, 249 }, 250 }, 251 expectedErr: "123456 is not a valid domain", 252 }, 253 { 254 name: "negative MTU", 255 config: &Config{ 256 CommonConfig: CommonConfig{ 257 Mtu: -10, 258 }, 259 }, 260 expectedErr: "invalid default MTU: -10", 261 }, 262 { 263 name: "negative max-concurrent-downloads", 264 config: &Config{ 265 CommonConfig: CommonConfig{ 266 MaxConcurrentDownloads: -10, 267 }, 268 }, 269 expectedErr: "invalid max concurrent downloads: -10", 270 }, 271 { 272 name: "negative max-concurrent-uploads", 273 config: &Config{ 274 CommonConfig: CommonConfig{ 275 MaxConcurrentUploads: -10, 276 }, 277 }, 278 expectedErr: "invalid max concurrent uploads: -10", 279 }, 280 { 281 name: "negative max-download-attempts", 282 config: &Config{ 283 CommonConfig: CommonConfig{ 284 MaxDownloadAttempts: -10, 285 }, 286 }, 287 expectedErr: "invalid max download attempts: -10", 288 }, 289 // TODO(thaJeztah) temporarily excluding this test as it assumes defaults are set before validating and applying updated configs 290 /* 291 { 292 name: "zero max-download-attempts", 293 field: "MaxDownloadAttempts", 294 config: &Config{ 295 CommonConfig: CommonConfig{ 296 MaxDownloadAttempts: 0, 297 }, 298 }, 299 expectedErr: "invalid max download attempts: 0", 300 }, 301 */ 302 { 303 name: "generic resource without =", 304 config: &Config{ 305 CommonConfig: CommonConfig{ 306 NodeGenericResources: []string{"foo"}, 307 }, 308 }, 309 expectedErr: "could not parse GenericResource: incorrect term foo, missing '=' or malformed expression", 310 }, 311 { 312 name: "generic resource mixed named and discrete", 313 config: &Config{ 314 CommonConfig: CommonConfig{ 315 NodeGenericResources: []string{"foo=bar", "foo=1"}, 316 }, 317 }, 318 expectedErr: "could not parse GenericResource: mixed discrete and named resources in expression 'foo=[bar 1]'", 319 }, 320 { 321 name: "with invalid hosts", 322 config: &Config{ 323 CommonConfig: CommonConfig{ 324 Hosts: []string{"127.0.0.1:2375/path"}, 325 }, 326 }, 327 expectedErr: "invalid bind address (127.0.0.1:2375/path): should not contain a path element", 328 }, 329 { 330 name: "with invalid log-level", 331 config: &Config{ 332 CommonConfig: CommonConfig{ 333 LogLevel: "foobar", 334 }, 335 }, 336 expectedErr: "invalid logging level: foobar", 337 }, 338 } 339 for _, tc := range testCases { 340 t.Run(tc.name, func(t *testing.T) { 341 cfg, err := New() 342 assert.NilError(t, err) 343 if tc.field != "" { 344 assert.Check(t, mergo.Merge(cfg, tc.config, mergo.WithOverride, withForceOverwrite(tc.field))) 345 } else { 346 assert.Check(t, mergo.Merge(cfg, tc.config, mergo.WithOverride)) 347 } 348 err = Validate(cfg) 349 assert.Error(t, err, tc.expectedErr) 350 }) 351 } 352 } 353 354 func withForceOverwrite(fieldName string) func(config *mergo.Config) { 355 return mergo.WithTransformers(overwriteTransformer{fieldName: fieldName}) 356 } 357 358 type overwriteTransformer struct { 359 fieldName string 360 } 361 362 func (tf overwriteTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error { 363 if typ == reflect.TypeOf(CommonConfig{}) { 364 return func(dst, src reflect.Value) error { 365 dst.FieldByName(tf.fieldName).Set(src.FieldByName(tf.fieldName)) 366 return nil 367 } 368 } 369 return nil 370 } 371 372 func TestValidateConfiguration(t *testing.T) { 373 testCases := []struct { 374 name string 375 field string 376 config *Config 377 }{ 378 { 379 name: "with label", 380 field: "Labels", 381 config: &Config{ 382 CommonConfig: CommonConfig{ 383 Labels: []string{"one=two"}, 384 }, 385 }, 386 }, 387 { 388 name: "with dns", 389 field: "DNSConfig", 390 config: &Config{ 391 CommonConfig: CommonConfig{ 392 DNSConfig: DNSConfig{ 393 DNS: []string{"1.1.1.1"}, 394 }, 395 }, 396 }, 397 }, 398 { 399 name: "with dns-search", 400 field: "DNSConfig", 401 config: &Config{ 402 CommonConfig: CommonConfig{ 403 DNSConfig: DNSConfig{ 404 DNSSearch: []string{"a.b.c"}, 405 }, 406 }, 407 }, 408 }, 409 { 410 name: "with mtu", 411 field: "Mtu", 412 config: &Config{ 413 CommonConfig: CommonConfig{ 414 Mtu: 1234, 415 }, 416 }, 417 }, 418 { 419 name: "with max-concurrent-downloads", 420 field: "MaxConcurrentDownloads", 421 config: &Config{ 422 CommonConfig: CommonConfig{ 423 MaxConcurrentDownloads: 4, 424 }, 425 }, 426 }, 427 { 428 name: "with max-concurrent-uploads", 429 field: "MaxConcurrentUploads", 430 config: &Config{ 431 CommonConfig: CommonConfig{ 432 MaxConcurrentUploads: 4, 433 }, 434 }, 435 }, 436 { 437 name: "with max-download-attempts", 438 field: "MaxDownloadAttempts", 439 config: &Config{ 440 CommonConfig: CommonConfig{ 441 MaxDownloadAttempts: 4, 442 }, 443 }, 444 }, 445 { 446 name: "with multiple node generic resources", 447 field: "NodeGenericResources", 448 config: &Config{ 449 CommonConfig: CommonConfig{ 450 NodeGenericResources: []string{"foo=bar", "foo=baz"}, 451 }, 452 }, 453 }, 454 { 455 name: "with node generic resources", 456 field: "NodeGenericResources", 457 config: &Config{ 458 CommonConfig: CommonConfig{ 459 NodeGenericResources: []string{"foo=1"}, 460 }, 461 }, 462 }, 463 { 464 name: "with hosts", 465 field: "Hosts", 466 config: &Config{ 467 CommonConfig: CommonConfig{ 468 Hosts: []string{"tcp://127.0.0.1:2375"}, 469 }, 470 }, 471 }, 472 { 473 name: "with log-level warn", 474 field: "LogLevel", 475 config: &Config{ 476 CommonConfig: CommonConfig{ 477 LogLevel: "warn", 478 }, 479 }, 480 }, 481 } 482 for _, tc := range testCases { 483 t.Run(tc.name, func(t *testing.T) { 484 // Start with a config with all defaults set, so that we only 485 cfg, err := New() 486 assert.NilError(t, err) 487 assert.Check(t, mergo.Merge(cfg, tc.config, mergo.WithOverride)) 488 489 // Check that the override happened :) 490 assert.Check(t, is.DeepEqual(cfg, tc.config, field(tc.field))) 491 err = Validate(cfg) 492 assert.NilError(t, err) 493 }) 494 } 495 } 496 497 func field(field string) cmp.Option { 498 tmp := reflect.TypeOf(Config{}) 499 ignoreFields := make([]string, 0, tmp.NumField()) 500 for i := 0; i < tmp.NumField(); i++ { 501 if tmp.Field(i).Name != field { 502 ignoreFields = append(ignoreFields, tmp.Field(i).Name) 503 } 504 } 505 return cmpopts.IgnoreFields(Config{}, ignoreFields...) 506 } 507 508 // TestReloadSetConfigFileNotExist tests that when `--config-file` is set 509 // and it doesn't exist the `Reload` function returns an error. 510 func TestReloadSetConfigFileNotExist(t *testing.T) { 511 configFile := "/tmp/blabla/not/exists/config.json" 512 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 513 flags.String("config-file", "", "") 514 assert.Check(t, flags.Set("config-file", configFile)) 515 516 err := Reload(configFile, flags, func(c *Config) {}) 517 assert.Check(t, is.ErrorContains(err, "unable to configure the Docker daemon with file")) 518 } 519 520 // TestReloadDefaultConfigNotExist tests that if the default configuration file 521 // doesn't exist the daemon still will be reloaded. 522 func TestReloadDefaultConfigNotExist(t *testing.T) { 523 skip.If(t, os.Getuid() != 0, "skipping test that requires root") 524 defaultConfigFile := "/tmp/blabla/not/exists/daemon.json" 525 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 526 flags.String("config-file", defaultConfigFile, "") 527 reloaded := false 528 err := Reload(defaultConfigFile, flags, func(c *Config) { 529 reloaded = true 530 }) 531 assert.Check(t, err) 532 assert.Check(t, reloaded) 533 } 534 535 // TestReloadBadDefaultConfig tests that when `--config-file` is not set 536 // and the default configuration file exists and is bad return an error 537 func TestReloadBadDefaultConfig(t *testing.T) { 538 f, err := os.CreateTemp("", "docker-config-") 539 assert.NilError(t, err) 540 541 configFile := f.Name() 542 f.Write([]byte(`{wrong: "configuration"}`)) 543 f.Close() 544 545 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 546 flags.String("config-file", configFile, "") 547 reloaded := false 548 err = Reload(configFile, flags, func(c *Config) { 549 reloaded = true 550 }) 551 assert.Check(t, is.ErrorContains(err, "unable to configure the Docker daemon with file")) 552 assert.Check(t, reloaded == false) 553 } 554 555 func TestReloadWithConflictingLabels(t *testing.T) { 556 tempFile := fs.NewFile(t, "config", fs.WithContent(`{"labels":["foo=bar","foo=baz"]}`)) 557 defer tempFile.Remove() 558 configFile := tempFile.Path() 559 560 var lbls []string 561 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 562 flags.String("config-file", configFile, "") 563 flags.StringSlice("labels", lbls, "") 564 reloaded := false 565 err := Reload(configFile, flags, func(c *Config) { 566 reloaded = true 567 }) 568 assert.Check(t, is.ErrorContains(err, "conflict labels for foo=baz and foo=bar")) 569 assert.Check(t, reloaded == false) 570 } 571 572 func TestReloadWithDuplicateLabels(t *testing.T) { 573 tempFile := fs.NewFile(t, "config", fs.WithContent(`{"labels":["foo=the-same","foo=the-same"]}`)) 574 defer tempFile.Remove() 575 configFile := tempFile.Path() 576 577 var lbls []string 578 flags := pflag.NewFlagSet("test", pflag.ContinueOnError) 579 flags.String("config-file", configFile, "") 580 flags.StringSlice("labels", lbls, "") 581 reloaded := false 582 err := Reload(configFile, flags, func(c *Config) { 583 reloaded = true 584 assert.Check(t, is.DeepEqual(c.Labels, []string{"foo=the-same"})) 585 }) 586 assert.Check(t, err) 587 assert.Check(t, reloaded) 588 } 589 590 func TestMaskURLCredentials(t *testing.T) { 591 tests := []struct { 592 rawURL string 593 maskedURL string 594 }{ 595 { 596 rawURL: "", 597 maskedURL: "", 598 }, { 599 rawURL: "invalidURL", 600 maskedURL: "invalidURL", 601 }, { 602 rawURL: "http://proxy.example.com:80/", 603 maskedURL: "http://proxy.example.com:80/", 604 }, { 605 rawURL: "http://USER:PASSWORD@proxy.example.com:80/", 606 maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/", 607 }, { 608 rawURL: "http://PASSWORD:PASSWORD@proxy.example.com:80/", 609 maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/", 610 }, { 611 rawURL: "http://USER:@proxy.example.com:80/", 612 maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/", 613 }, { 614 rawURL: "http://:PASSWORD@proxy.example.com:80/", 615 maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/", 616 }, { 617 rawURL: "http://USER@docker:password@proxy.example.com:80/", 618 maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/", 619 }, { 620 rawURL: "http://USER%40docker:password@proxy.example.com:80/", 621 maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/", 622 }, { 623 rawURL: "http://USER%40docker:pa%3Fsword@proxy.example.com:80/", 624 maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/", 625 }, { 626 rawURL: "http://USER%40docker:pa%3Fsword@proxy.example.com:80/hello%20world", 627 maskedURL: "http://xxxxx:xxxxx@proxy.example.com:80/hello%20world", 628 }, 629 } 630 for _, test := range tests { 631 maskedURL := MaskCredentials(test.rawURL) 632 assert.Equal(t, maskedURL, test.maskedURL) 633 } 634 }