github.com/justincormack/cli@v0.0.0-20201215022714-831ebeae9675/cli/config/configfile/file_test.go (about) 1 package configfile 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "io/ioutil" 7 "os" 8 "testing" 9 10 "github.com/docker/cli/cli/config/credentials" 11 "github.com/docker/cli/cli/config/types" 12 "gotest.tools/v3/assert" 13 is "gotest.tools/v3/assert/cmp" 14 "gotest.tools/v3/fs" 15 "gotest.tools/v3/golden" 16 ) 17 18 func TestEncodeAuth(t *testing.T) { 19 newAuthConfig := &types.AuthConfig{Username: "ken", Password: "test"} 20 authStr := encodeAuth(newAuthConfig) 21 22 expected := &types.AuthConfig{} 23 var err error 24 expected.Username, expected.Password, err = decodeAuth(authStr) 25 assert.NilError(t, err) 26 assert.Check(t, is.DeepEqual(expected, newAuthConfig)) 27 } 28 29 func TestProxyConfig(t *testing.T) { 30 httpProxy := "http://proxy.mycorp.com:3128" 31 httpsProxy := "https://user:password@proxy.mycorp.com:3129" 32 ftpProxy := "http://ftpproxy.mycorp.com:21" 33 noProxy := "*.intra.mycorp.com" 34 defaultProxyConfig := ProxyConfig{ 35 HTTPProxy: httpProxy, 36 HTTPSProxy: httpsProxy, 37 FTPProxy: ftpProxy, 38 NoProxy: noProxy, 39 } 40 41 cfg := ConfigFile{ 42 Proxies: map[string]ProxyConfig{ 43 "default": defaultProxyConfig, 44 }, 45 } 46 47 proxyConfig := cfg.ParseProxyConfig("/var/run/docker.sock", nil) 48 expected := map[string]*string{ 49 "HTTP_PROXY": &httpProxy, 50 "http_proxy": &httpProxy, 51 "HTTPS_PROXY": &httpsProxy, 52 "https_proxy": &httpsProxy, 53 "FTP_PROXY": &ftpProxy, 54 "ftp_proxy": &ftpProxy, 55 "NO_PROXY": &noProxy, 56 "no_proxy": &noProxy, 57 } 58 assert.Check(t, is.DeepEqual(expected, proxyConfig)) 59 } 60 61 func TestProxyConfigOverride(t *testing.T) { 62 httpProxy := "http://proxy.mycorp.com:3128" 63 overrideHTTPProxy := "http://proxy.example.com:3128" 64 overrideNoProxy := "" 65 httpsProxy := "https://user:password@proxy.mycorp.com:3129" 66 ftpProxy := "http://ftpproxy.mycorp.com:21" 67 noProxy := "*.intra.mycorp.com" 68 defaultProxyConfig := ProxyConfig{ 69 HTTPProxy: httpProxy, 70 HTTPSProxy: httpsProxy, 71 FTPProxy: ftpProxy, 72 NoProxy: noProxy, 73 } 74 75 cfg := ConfigFile{ 76 Proxies: map[string]ProxyConfig{ 77 "default": defaultProxyConfig, 78 }, 79 } 80 81 clone := func(s string) *string { 82 s2 := s 83 return &s2 84 } 85 86 ropts := map[string]*string{ 87 "HTTP_PROXY": clone(overrideHTTPProxy), 88 "NO_PROXY": clone(overrideNoProxy), 89 } 90 proxyConfig := cfg.ParseProxyConfig("/var/run/docker.sock", ropts) 91 expected := map[string]*string{ 92 "HTTP_PROXY": &overrideHTTPProxy, 93 "http_proxy": &httpProxy, 94 "HTTPS_PROXY": &httpsProxy, 95 "https_proxy": &httpsProxy, 96 "FTP_PROXY": &ftpProxy, 97 "ftp_proxy": &ftpProxy, 98 "NO_PROXY": &overrideNoProxy, 99 "no_proxy": &noProxy, 100 } 101 assert.Check(t, is.DeepEqual(expected, proxyConfig)) 102 } 103 104 func TestProxyConfigPerHost(t *testing.T) { 105 httpProxy := "http://proxy.mycorp.com:3128" 106 httpsProxy := "https://user:password@proxy.mycorp.com:3129" 107 ftpProxy := "http://ftpproxy.mycorp.com:21" 108 noProxy := "*.intra.mycorp.com" 109 110 extHTTPProxy := "http://proxy.example.com:3128" 111 extHTTPSProxy := "https://user:password@proxy.example.com:3129" 112 extFTPProxy := "http://ftpproxy.example.com:21" 113 extNoProxy := "*.intra.example.com" 114 115 defaultProxyConfig := ProxyConfig{ 116 HTTPProxy: httpProxy, 117 HTTPSProxy: httpsProxy, 118 FTPProxy: ftpProxy, 119 NoProxy: noProxy, 120 } 121 externalProxyConfig := ProxyConfig{ 122 HTTPProxy: extHTTPProxy, 123 HTTPSProxy: extHTTPSProxy, 124 FTPProxy: extFTPProxy, 125 NoProxy: extNoProxy, 126 } 127 128 cfg := ConfigFile{ 129 Proxies: map[string]ProxyConfig{ 130 "default": defaultProxyConfig, 131 "tcp://example.docker.com:2376": externalProxyConfig, 132 }, 133 } 134 135 proxyConfig := cfg.ParseProxyConfig("tcp://example.docker.com:2376", nil) 136 expected := map[string]*string{ 137 "HTTP_PROXY": &extHTTPProxy, 138 "http_proxy": &extHTTPProxy, 139 "HTTPS_PROXY": &extHTTPSProxy, 140 "https_proxy": &extHTTPSProxy, 141 "FTP_PROXY": &extFTPProxy, 142 "ftp_proxy": &extFTPProxy, 143 "NO_PROXY": &extNoProxy, 144 "no_proxy": &extNoProxy, 145 } 146 assert.Check(t, is.DeepEqual(expected, proxyConfig)) 147 } 148 149 func TestConfigFile(t *testing.T) { 150 configFilename := "configFilename" 151 configFile := New(configFilename) 152 153 assert.Check(t, is.Equal(configFilename, configFile.Filename)) 154 } 155 156 type mockNativeStore struct { 157 GetAllCallCount int 158 authConfigs map[string]types.AuthConfig 159 } 160 161 func (c *mockNativeStore) Erase(registryHostname string) error { 162 delete(c.authConfigs, registryHostname) 163 return nil 164 } 165 166 func (c *mockNativeStore) Get(registryHostname string) (types.AuthConfig, error) { 167 return c.authConfigs[registryHostname], nil 168 } 169 170 func (c *mockNativeStore) GetAll() (map[string]types.AuthConfig, error) { 171 c.GetAllCallCount = c.GetAllCallCount + 1 172 return c.authConfigs, nil 173 } 174 175 func (c *mockNativeStore) Store(authConfig types.AuthConfig) error { 176 return nil 177 } 178 179 // make sure it satisfies the interface 180 var _ credentials.Store = (*mockNativeStore)(nil) 181 182 func NewMockNativeStore(authConfigs map[string]types.AuthConfig) credentials.Store { 183 return &mockNativeStore{authConfigs: authConfigs} 184 } 185 186 func TestGetAllCredentialsFileStoreOnly(t *testing.T) { 187 configFile := New("filename") 188 exampleAuth := types.AuthConfig{ 189 Username: "user", 190 Password: "pass", 191 } 192 configFile.AuthConfigs["example.com/foo"] = exampleAuth 193 194 authConfigs, err := configFile.GetAllCredentials() 195 assert.NilError(t, err) 196 197 expected := make(map[string]types.AuthConfig) 198 expected["example.com/foo"] = exampleAuth 199 assert.Check(t, is.DeepEqual(expected, authConfigs)) 200 } 201 202 func TestGetAllCredentialsCredsStore(t *testing.T) { 203 configFile := New("filename") 204 configFile.CredentialsStore = "test_creds_store" 205 testRegistryHostname := "example.com" 206 expectedAuth := types.AuthConfig{ 207 Username: "user", 208 Password: "pass", 209 } 210 211 testCredsStore := NewMockNativeStore(map[string]types.AuthConfig{testRegistryHostname: expectedAuth}) 212 213 tmpNewNativeStore := newNativeStore 214 defer func() { newNativeStore = tmpNewNativeStore }() 215 newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store { 216 return testCredsStore 217 } 218 219 authConfigs, err := configFile.GetAllCredentials() 220 assert.NilError(t, err) 221 222 expected := make(map[string]types.AuthConfig) 223 expected[testRegistryHostname] = expectedAuth 224 assert.Check(t, is.DeepEqual(expected, authConfigs)) 225 assert.Check(t, is.Equal(1, testCredsStore.(*mockNativeStore).GetAllCallCount)) 226 } 227 228 func TestGetAllCredentialsCredHelper(t *testing.T) { 229 testCredHelperSuffix := "test_cred_helper" 230 testCredHelperRegistryHostname := "credhelper.com" 231 testExtraCredHelperRegistryHostname := "somethingweird.com" 232 233 unexpectedCredHelperAuth := types.AuthConfig{ 234 Username: "file_store_user", 235 Password: "file_store_pass", 236 } 237 expectedCredHelperAuth := types.AuthConfig{ 238 Username: "cred_helper_user", 239 Password: "cred_helper_pass", 240 } 241 242 configFile := New("filename") 243 configFile.CredentialHelpers = map[string]string{testCredHelperRegistryHostname: testCredHelperSuffix} 244 245 testCredHelper := NewMockNativeStore(map[string]types.AuthConfig{ 246 testCredHelperRegistryHostname: expectedCredHelperAuth, 247 // Add in an extra auth entry which doesn't appear in CredentialHelpers section of the configFile. 248 // This verifies that only explicitly configured registries are being requested from the cred helpers. 249 testExtraCredHelperRegistryHostname: unexpectedCredHelperAuth, 250 }) 251 252 tmpNewNativeStore := newNativeStore 253 defer func() { newNativeStore = tmpNewNativeStore }() 254 newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store { 255 return testCredHelper 256 } 257 258 authConfigs, err := configFile.GetAllCredentials() 259 assert.NilError(t, err) 260 261 expected := make(map[string]types.AuthConfig) 262 expected[testCredHelperRegistryHostname] = expectedCredHelperAuth 263 assert.Check(t, is.DeepEqual(expected, authConfigs)) 264 assert.Check(t, is.Equal(0, testCredHelper.(*mockNativeStore).GetAllCallCount)) 265 } 266 267 func TestGetAllCredentialsFileStoreAndCredHelper(t *testing.T) { 268 testFileStoreRegistryHostname := "example.com" 269 testCredHelperSuffix := "test_cred_helper" 270 testCredHelperRegistryHostname := "credhelper.com" 271 272 expectedFileStoreAuth := types.AuthConfig{ 273 Username: "file_store_user", 274 Password: "file_store_pass", 275 } 276 expectedCredHelperAuth := types.AuthConfig{ 277 Username: "cred_helper_user", 278 Password: "cred_helper_pass", 279 } 280 281 configFile := New("filename") 282 configFile.CredentialHelpers = map[string]string{testCredHelperRegistryHostname: testCredHelperSuffix} 283 configFile.AuthConfigs[testFileStoreRegistryHostname] = expectedFileStoreAuth 284 285 testCredHelper := NewMockNativeStore(map[string]types.AuthConfig{testCredHelperRegistryHostname: expectedCredHelperAuth}) 286 287 newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store { 288 return testCredHelper 289 } 290 291 tmpNewNativeStore := newNativeStore 292 defer func() { newNativeStore = tmpNewNativeStore }() 293 authConfigs, err := configFile.GetAllCredentials() 294 assert.NilError(t, err) 295 296 expected := make(map[string]types.AuthConfig) 297 expected[testFileStoreRegistryHostname] = expectedFileStoreAuth 298 expected[testCredHelperRegistryHostname] = expectedCredHelperAuth 299 assert.Check(t, is.DeepEqual(expected, authConfigs)) 300 assert.Check(t, is.Equal(0, testCredHelper.(*mockNativeStore).GetAllCallCount)) 301 } 302 303 func TestGetAllCredentialsCredStoreAndCredHelper(t *testing.T) { 304 testCredStoreSuffix := "test_creds_store" 305 testCredStoreRegistryHostname := "credstore.com" 306 testCredHelperSuffix := "test_cred_helper" 307 testCredHelperRegistryHostname := "credhelper.com" 308 309 configFile := New("filename") 310 configFile.CredentialsStore = testCredStoreSuffix 311 configFile.CredentialHelpers = map[string]string{testCredHelperRegistryHostname: testCredHelperSuffix} 312 313 expectedCredStoreAuth := types.AuthConfig{ 314 Username: "cred_store_user", 315 Password: "cred_store_pass", 316 } 317 expectedCredHelperAuth := types.AuthConfig{ 318 Username: "cred_helper_user", 319 Password: "cred_helper_pass", 320 } 321 322 testCredHelper := NewMockNativeStore(map[string]types.AuthConfig{testCredHelperRegistryHostname: expectedCredHelperAuth}) 323 testCredsStore := NewMockNativeStore(map[string]types.AuthConfig{testCredStoreRegistryHostname: expectedCredStoreAuth}) 324 325 tmpNewNativeStore := newNativeStore 326 defer func() { newNativeStore = tmpNewNativeStore }() 327 newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store { 328 if helperSuffix == testCredHelperSuffix { 329 return testCredHelper 330 } 331 return testCredsStore 332 } 333 334 authConfigs, err := configFile.GetAllCredentials() 335 assert.NilError(t, err) 336 337 expected := make(map[string]types.AuthConfig) 338 expected[testCredStoreRegistryHostname] = expectedCredStoreAuth 339 expected[testCredHelperRegistryHostname] = expectedCredHelperAuth 340 assert.Check(t, is.DeepEqual(expected, authConfigs)) 341 assert.Check(t, is.Equal(1, testCredsStore.(*mockNativeStore).GetAllCallCount)) 342 assert.Check(t, is.Equal(0, testCredHelper.(*mockNativeStore).GetAllCallCount)) 343 } 344 345 func TestGetAllCredentialsCredHelperOverridesDefaultStore(t *testing.T) { 346 testCredStoreSuffix := "test_creds_store" 347 testCredHelperSuffix := "test_cred_helper" 348 testRegistryHostname := "example.com" 349 350 configFile := New("filename") 351 configFile.CredentialsStore = testCredStoreSuffix 352 configFile.CredentialHelpers = map[string]string{testRegistryHostname: testCredHelperSuffix} 353 354 unexpectedCredStoreAuth := types.AuthConfig{ 355 Username: "cred_store_user", 356 Password: "cred_store_pass", 357 } 358 expectedCredHelperAuth := types.AuthConfig{ 359 Username: "cred_helper_user", 360 Password: "cred_helper_pass", 361 } 362 363 testCredHelper := NewMockNativeStore(map[string]types.AuthConfig{testRegistryHostname: expectedCredHelperAuth}) 364 testCredsStore := NewMockNativeStore(map[string]types.AuthConfig{testRegistryHostname: unexpectedCredStoreAuth}) 365 366 tmpNewNativeStore := newNativeStore 367 defer func() { newNativeStore = tmpNewNativeStore }() 368 newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store { 369 if helperSuffix == testCredHelperSuffix { 370 return testCredHelper 371 } 372 return testCredsStore 373 } 374 375 authConfigs, err := configFile.GetAllCredentials() 376 assert.NilError(t, err) 377 378 expected := make(map[string]types.AuthConfig) 379 expected[testRegistryHostname] = expectedCredHelperAuth 380 assert.Check(t, is.DeepEqual(expected, authConfigs)) 381 assert.Check(t, is.Equal(1, testCredsStore.(*mockNativeStore).GetAllCallCount)) 382 assert.Check(t, is.Equal(0, testCredHelper.(*mockNativeStore).GetAllCallCount)) 383 } 384 385 func TestLoadFromReaderWithUsernamePassword(t *testing.T) { 386 configFile := New("test-load") 387 defer os.Remove("test-load") 388 389 want := types.AuthConfig{ 390 Username: "user", 391 Password: "pass", 392 } 393 394 for _, tc := range []types.AuthConfig{ 395 want, 396 { 397 Auth: encodeAuth(&want), 398 }, 399 } { 400 cf := ConfigFile{ 401 AuthConfigs: map[string]types.AuthConfig{ 402 "example.com/foo": tc, 403 }, 404 } 405 406 b, err := json.Marshal(cf) 407 assert.NilError(t, err) 408 409 err = configFile.LoadFromReader(bytes.NewReader(b)) 410 assert.NilError(t, err) 411 412 got, err := configFile.GetAuthConfig("example.com/foo") 413 assert.NilError(t, err) 414 415 assert.Check(t, is.DeepEqual(want.Username, got.Username)) 416 assert.Check(t, is.DeepEqual(want.Password, got.Password)) 417 } 418 } 419 420 func TestCheckKubernetesConfigurationRaiseAnErrorOnInvalidValue(t *testing.T) { 421 testCases := []struct { 422 name string 423 config *KubernetesConfig 424 expectError bool 425 }{ 426 { 427 "no kubernetes config is valid", 428 nil, 429 false, 430 }, 431 { 432 "enabled is valid", 433 &KubernetesConfig{AllNamespaces: "enabled"}, 434 false, 435 }, 436 { 437 "disabled is valid", 438 &KubernetesConfig{AllNamespaces: "disabled"}, 439 false, 440 }, 441 { 442 "empty string is valid", 443 &KubernetesConfig{AllNamespaces: ""}, 444 false, 445 }, 446 { 447 "other value is invalid", 448 &KubernetesConfig{AllNamespaces: "unknown"}, 449 true, 450 }, 451 } 452 for _, test := range testCases { 453 err := checkKubernetesConfiguration(test.config) 454 if test.expectError { 455 assert.Assert(t, err != nil, test.name) 456 } else { 457 assert.NilError(t, err, test.name) 458 } 459 } 460 } 461 462 func TestSave(t *testing.T) { 463 configFile := New("test-save") 464 defer os.Remove("test-save") 465 err := configFile.Save() 466 assert.NilError(t, err) 467 cfg, err := ioutil.ReadFile("test-save") 468 assert.NilError(t, err) 469 assert.Equal(t, string(cfg), `{ 470 "auths": {} 471 }`) 472 } 473 474 func TestSaveCustomHTTPHeaders(t *testing.T) { 475 configFile := New(t.Name()) 476 defer os.Remove(t.Name()) 477 configFile.HTTPHeaders["CUSTOM-HEADER"] = "custom-value" 478 configFile.HTTPHeaders["User-Agent"] = "user-agent 1" 479 configFile.HTTPHeaders["user-agent"] = "user-agent 2" 480 err := configFile.Save() 481 assert.NilError(t, err) 482 cfg, err := ioutil.ReadFile(t.Name()) 483 assert.NilError(t, err) 484 assert.Equal(t, string(cfg), `{ 485 "auths": {}, 486 "HttpHeaders": { 487 "CUSTOM-HEADER": "custom-value" 488 } 489 }`) 490 } 491 492 func TestSaveWithSymlink(t *testing.T) { 493 dir := fs.NewDir(t, t.Name(), fs.WithFile("real-config.json", `{}`)) 494 defer dir.Remove() 495 496 symLink := dir.Join("config.json") 497 realFile := dir.Join("real-config.json") 498 err := os.Symlink(realFile, symLink) 499 assert.NilError(t, err) 500 501 configFile := New(symLink) 502 503 err = configFile.Save() 504 assert.NilError(t, err) 505 506 fi, err := os.Lstat(symLink) 507 assert.NilError(t, err) 508 assert.Assert(t, fi.Mode()&os.ModeSymlink != 0, "expected %s to be a symlink", symLink) 509 510 cfg, err := ioutil.ReadFile(symLink) 511 assert.NilError(t, err) 512 assert.Check(t, is.Equal(string(cfg), "{\n \"auths\": {}\n}")) 513 514 cfg, err = ioutil.ReadFile(realFile) 515 assert.NilError(t, err) 516 assert.Check(t, is.Equal(string(cfg), "{\n \"auths\": {}\n}")) 517 } 518 519 func TestPluginConfig(t *testing.T) { 520 configFile := New("test-plugin") 521 defer os.Remove("test-plugin") 522 523 // Populate some initial values 524 configFile.SetPluginConfig("plugin1", "data1", "some string") 525 configFile.SetPluginConfig("plugin1", "data2", "42") 526 configFile.SetPluginConfig("plugin2", "data3", "some other string") 527 528 // Save a config file with some plugin config 529 err := configFile.Save() 530 assert.NilError(t, err) 531 532 // Read it back and check it has the expected content 533 cfg, err := ioutil.ReadFile("test-plugin") 534 assert.NilError(t, err) 535 golden.Assert(t, string(cfg), "plugin-config.golden") 536 537 // Load it, resave and check again that the content is 538 // preserved through a load/save cycle. 539 configFile = New("test-plugin2") 540 defer os.Remove("test-plugin2") 541 assert.NilError(t, configFile.LoadFromReader(bytes.NewReader(cfg))) 542 err = configFile.Save() 543 assert.NilError(t, err) 544 cfg, err = ioutil.ReadFile("test-plugin2") 545 assert.NilError(t, err) 546 golden.Assert(t, string(cfg), "plugin-config.golden") 547 548 // Check that the contents was reloaded properly 549 v, ok := configFile.PluginConfig("plugin1", "data1") 550 assert.Assert(t, ok) 551 assert.Equal(t, v, "some string") 552 v, ok = configFile.PluginConfig("plugin1", "data2") 553 assert.Assert(t, ok) 554 assert.Equal(t, v, "42") 555 v, ok = configFile.PluginConfig("plugin1", "data3") 556 assert.Assert(t, !ok) 557 assert.Equal(t, v, "") 558 v, ok = configFile.PluginConfig("plugin2", "data3") 559 assert.Assert(t, ok) 560 assert.Equal(t, v, "some other string") 561 v, ok = configFile.PluginConfig("plugin2", "data4") 562 assert.Assert(t, !ok) 563 assert.Equal(t, v, "") 564 v, ok = configFile.PluginConfig("plugin3", "data5") 565 assert.Assert(t, !ok) 566 assert.Equal(t, v, "") 567 568 // Add, remove and modify 569 configFile.SetPluginConfig("plugin1", "data1", "some replacement string") // replacing a key 570 configFile.SetPluginConfig("plugin1", "data2", "") // deleting a key 571 configFile.SetPluginConfig("plugin1", "data3", "some additional string") // new key 572 configFile.SetPluginConfig("plugin2", "data3", "") // delete the whole plugin, since this was the only data 573 configFile.SetPluginConfig("plugin3", "data5", "a new plugin") // add a new plugin 574 575 err = configFile.Save() 576 assert.NilError(t, err) 577 578 // Read it back and check it has the expected content again 579 cfg, err = ioutil.ReadFile("test-plugin2") 580 assert.NilError(t, err) 581 golden.Assert(t, string(cfg), "plugin-config-2.golden") 582 }