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