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