k8s.io/kubernetes@v1.29.3/pkg/credentialprovider/plugin/plugin_test.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package plugin 18 19 import ( 20 "context" 21 "fmt" 22 "reflect" 23 "sync" 24 "testing" 25 "time" 26 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/runtime" 29 "k8s.io/apimachinery/pkg/runtime/schema" 30 "k8s.io/apimachinery/pkg/util/rand" 31 "k8s.io/client-go/tools/cache" 32 credentialproviderapi "k8s.io/kubelet/pkg/apis/credentialprovider" 33 credentialproviderv1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1" 34 credentialproviderv1alpha1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1" 35 credentialproviderv1beta1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1beta1" 36 "k8s.io/kubernetes/pkg/credentialprovider" 37 kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config" 38 "k8s.io/utils/clock" 39 testingclock "k8s.io/utils/clock/testing" 40 ) 41 42 type fakeExecPlugin struct { 43 cacheKeyType credentialproviderapi.PluginCacheKeyType 44 cacheDuration time.Duration 45 46 auth map[string]credentialproviderapi.AuthConfig 47 } 48 49 func (f *fakeExecPlugin) ExecPlugin(ctx context.Context, image string) (*credentialproviderapi.CredentialProviderResponse, error) { 50 return &credentialproviderapi.CredentialProviderResponse{ 51 CacheKeyType: f.cacheKeyType, 52 CacheDuration: &metav1.Duration{ 53 Duration: f.cacheDuration, 54 }, 55 Auth: f.auth, 56 }, nil 57 } 58 59 func Test_Provide(t *testing.T) { 60 tclock := clock.RealClock{} 61 testcases := []struct { 62 name string 63 pluginProvider *pluginProvider 64 image string 65 dockerconfig credentialprovider.DockerConfig 66 }{ 67 { 68 name: "exact image match, with Registry cache key", 69 pluginProvider: &pluginProvider{ 70 clock: tclock, 71 lastCachePurge: tclock.Now(), 72 matchImages: []string{"test.registry.io"}, 73 cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{clock: tclock}), 74 plugin: &fakeExecPlugin{ 75 cacheKeyType: credentialproviderapi.RegistryPluginCacheKeyType, 76 auth: map[string]credentialproviderapi.AuthConfig{ 77 "test.registry.io": { 78 Username: "user", 79 Password: "password", 80 }, 81 }, 82 }, 83 }, 84 image: "test.registry.io/foo/bar", 85 dockerconfig: credentialprovider.DockerConfig{ 86 "test.registry.io": credentialprovider.DockerConfigEntry{ 87 Username: "user", 88 Password: "password", 89 }, 90 }, 91 }, 92 { 93 name: "exact image match, with Image cache key", 94 pluginProvider: &pluginProvider{ 95 clock: tclock, 96 lastCachePurge: tclock.Now(), 97 matchImages: []string{"test.registry.io/foo/bar"}, 98 cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{clock: tclock}), 99 plugin: &fakeExecPlugin{ 100 cacheKeyType: credentialproviderapi.ImagePluginCacheKeyType, 101 auth: map[string]credentialproviderapi.AuthConfig{ 102 "test.registry.io/foo/bar": { 103 Username: "user", 104 Password: "password", 105 }, 106 }, 107 }, 108 }, 109 image: "test.registry.io/foo/bar", 110 dockerconfig: credentialprovider.DockerConfig{ 111 "test.registry.io/foo/bar": credentialprovider.DockerConfigEntry{ 112 Username: "user", 113 Password: "password", 114 }, 115 }, 116 }, 117 { 118 name: "exact image match, with Global cache key", 119 pluginProvider: &pluginProvider{ 120 clock: tclock, 121 lastCachePurge: tclock.Now(), 122 matchImages: []string{"test.registry.io"}, 123 cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{clock: tclock}), 124 plugin: &fakeExecPlugin{ 125 cacheKeyType: credentialproviderapi.GlobalPluginCacheKeyType, 126 auth: map[string]credentialproviderapi.AuthConfig{ 127 "test.registry.io": { 128 Username: "user", 129 Password: "password", 130 }, 131 }, 132 }, 133 }, 134 image: "test.registry.io", 135 dockerconfig: credentialprovider.DockerConfig{ 136 "test.registry.io": credentialprovider.DockerConfigEntry{ 137 Username: "user", 138 Password: "password", 139 }, 140 }, 141 }, 142 { 143 name: "wild card image match, with Registry cache key", 144 pluginProvider: &pluginProvider{ 145 clock: tclock, 146 lastCachePurge: tclock.Now(), 147 matchImages: []string{"*.registry.io:8080"}, 148 cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{clock: tclock}), 149 plugin: &fakeExecPlugin{ 150 cacheKeyType: credentialproviderapi.RegistryPluginCacheKeyType, 151 auth: map[string]credentialproviderapi.AuthConfig{ 152 "*.registry.io:8080": { 153 Username: "user", 154 Password: "password", 155 }, 156 }, 157 }, 158 }, 159 image: "test.registry.io:8080/foo", 160 dockerconfig: credentialprovider.DockerConfig{ 161 "*.registry.io:8080": credentialprovider.DockerConfigEntry{ 162 Username: "user", 163 Password: "password", 164 }, 165 }, 166 }, 167 { 168 name: "wild card image match, with Image cache key", 169 pluginProvider: &pluginProvider{ 170 clock: tclock, 171 lastCachePurge: tclock.Now(), 172 matchImages: []string{"*.*.registry.io"}, 173 cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{clock: tclock}), 174 plugin: &fakeExecPlugin{ 175 cacheKeyType: credentialproviderapi.ImagePluginCacheKeyType, 176 auth: map[string]credentialproviderapi.AuthConfig{ 177 "*.*.registry.io": { 178 Username: "user", 179 Password: "password", 180 }, 181 }, 182 }, 183 }, 184 image: "foo.bar.registry.io/foo/bar", 185 dockerconfig: credentialprovider.DockerConfig{ 186 "*.*.registry.io": credentialprovider.DockerConfigEntry{ 187 Username: "user", 188 Password: "password", 189 }, 190 }, 191 }, 192 { 193 name: "wild card image match, with Global cache key", 194 pluginProvider: &pluginProvider{ 195 clock: tclock, 196 lastCachePurge: tclock.Now(), 197 matchImages: []string{"*.registry.io"}, 198 cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{clock: tclock}), 199 plugin: &fakeExecPlugin{ 200 cacheKeyType: credentialproviderapi.GlobalPluginCacheKeyType, 201 auth: map[string]credentialproviderapi.AuthConfig{ 202 "*.registry.io": { 203 Username: "user", 204 Password: "password", 205 }, 206 }, 207 }, 208 }, 209 image: "test.registry.io", 210 dockerconfig: credentialprovider.DockerConfig{ 211 "*.registry.io": credentialprovider.DockerConfigEntry{ 212 Username: "user", 213 Password: "password", 214 }, 215 }, 216 }, 217 } 218 219 for _, testcase := range testcases { 220 testcase := testcase 221 t.Run(testcase.name, func(t *testing.T) { 222 t.Parallel() 223 dockerconfig := testcase.pluginProvider.Provide(testcase.image) 224 if !reflect.DeepEqual(dockerconfig, testcase.dockerconfig) { 225 t.Logf("actual docker config: %v", dockerconfig) 226 t.Logf("expected docker config: %v", testcase.dockerconfig) 227 t.Error("unexpected docker config") 228 } 229 }) 230 } 231 } 232 233 // This test calls Provide in parallel for different registries and images 234 // The purpose of this is to detect any race conditions while cache rw. 235 func Test_ProvideParallel(t *testing.T) { 236 tclock := clock.RealClock{} 237 238 testcases := []struct { 239 name string 240 registry string 241 }{ 242 { 243 name: "provide for registry 1", 244 registry: "test1.registry.io", 245 }, 246 { 247 name: "provide for registry 2", 248 registry: "test2.registry.io", 249 }, 250 { 251 name: "provide for registry 3", 252 registry: "test3.registry.io", 253 }, 254 { 255 name: "provide for registry 4", 256 registry: "test4.registry.io", 257 }, 258 } 259 260 pluginProvider := &pluginProvider{ 261 clock: tclock, 262 lastCachePurge: tclock.Now(), 263 matchImages: []string{"test1.registry.io", "test2.registry.io", "test3.registry.io", "test4.registry.io"}, 264 cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{clock: tclock}), 265 plugin: &fakeExecPlugin{ 266 cacheDuration: time.Minute * 1, 267 cacheKeyType: credentialproviderapi.RegistryPluginCacheKeyType, 268 auth: map[string]credentialproviderapi.AuthConfig{ 269 "test.registry.io": { 270 Username: "user", 271 Password: "password", 272 }, 273 }, 274 }, 275 } 276 277 dockerconfig := credentialprovider.DockerConfig{ 278 "test.registry.io": credentialprovider.DockerConfigEntry{ 279 Username: "user", 280 Password: "password", 281 }, 282 } 283 284 for _, testcase := range testcases { 285 testcase := testcase 286 t.Run(testcase.name, func(t *testing.T) { 287 t.Parallel() 288 var wg sync.WaitGroup 289 wg.Add(5) 290 291 for i := 0; i < 5; i++ { 292 go func(w *sync.WaitGroup) { 293 image := fmt.Sprintf(testcase.registry+"/%s", rand.String(5)) 294 dockerconfigResponse := pluginProvider.Provide(image) 295 if !reflect.DeepEqual(dockerconfigResponse, dockerconfig) { 296 t.Logf("actual docker config: %v", dockerconfigResponse) 297 t.Logf("expected docker config: %v", dockerconfig) 298 t.Error("unexpected docker config") 299 } 300 w.Done() 301 }(&wg) 302 } 303 wg.Wait() 304 305 }) 306 } 307 } 308 309 func Test_getCachedCredentials(t *testing.T) { 310 fakeClock := testingclock.NewFakeClock(time.Now()) 311 p := &pluginProvider{ 312 clock: fakeClock, 313 lastCachePurge: fakeClock.Now(), 314 cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{clock: fakeClock}), 315 plugin: &fakeExecPlugin{}, 316 } 317 318 testcases := []struct { 319 name string 320 step time.Duration 321 cacheEntry cacheEntry 322 expectedResponse credentialprovider.DockerConfig 323 keyLength int 324 getKey string 325 }{ 326 { 327 name: "It should return not expired credential", 328 step: 1 * time.Second, 329 keyLength: 1, 330 getKey: "image1", 331 expectedResponse: map[string]credentialprovider.DockerConfigEntry{ 332 "image1": { 333 Username: "user1", 334 Password: "pass1", 335 }, 336 }, 337 cacheEntry: cacheEntry{ 338 key: "image1", 339 expiresAt: fakeClock.Now().Add(1 * time.Minute), 340 credentials: map[string]credentialprovider.DockerConfigEntry{ 341 "image1": { 342 Username: "user1", 343 Password: "pass1", 344 }, 345 }, 346 }, 347 }, 348 349 { 350 name: "It should not return expired credential", 351 step: 2 * time.Minute, 352 getKey: "image2", 353 keyLength: 1, 354 cacheEntry: cacheEntry{ 355 key: "image2", 356 expiresAt: fakeClock.Now(), 357 credentials: map[string]credentialprovider.DockerConfigEntry{ 358 "image2": { 359 Username: "user2", 360 Password: "pass2", 361 }, 362 }, 363 }, 364 }, 365 366 { 367 name: "It should delete expired credential during purge", 368 step: 18 * time.Minute, 369 keyLength: 0, 370 // while get call for random, cache purge will be called and it will delete expired 371 // image3 credentials. We cannot use image3 as getKey here, as it will get deleted during 372 // get only, we will not be able verify the purge call. 373 getKey: "random", 374 cacheEntry: cacheEntry{ 375 key: "image3", 376 expiresAt: fakeClock.Now().Add(2 * time.Minute), 377 credentials: map[string]credentialprovider.DockerConfigEntry{ 378 "image3": { 379 Username: "user3", 380 Password: "pass3", 381 }, 382 }, 383 }, 384 }, 385 } 386 387 for _, tc := range testcases { 388 t.Run(tc.name, func(t *testing.T) { 389 p.cache.Add(&tc.cacheEntry) 390 fakeClock.Step(tc.step) 391 392 // getCachedCredentials returns unexpired credentials. 393 res, _, err := p.getCachedCredentials(tc.getKey) 394 if err != nil { 395 t.Errorf("Unexpected error %v", err) 396 } 397 if !reflect.DeepEqual(res, tc.expectedResponse) { 398 t.Logf("response %v", res) 399 t.Logf("expected response %v", tc.expectedResponse) 400 t.Errorf("Unexpected response") 401 } 402 403 // Listkeys returns all the keys present in cache including expired keys. 404 if len(p.cache.ListKeys()) != tc.keyLength { 405 t.Errorf("Unexpected cache key length") 406 } 407 }) 408 } 409 } 410 411 func Test_encodeRequest(t *testing.T) { 412 testcases := []struct { 413 name string 414 apiVersion schema.GroupVersion 415 request *credentialproviderapi.CredentialProviderRequest 416 expectedData []byte 417 expectedErr bool 418 }{ 419 { 420 name: "successful with v1alpha1", 421 apiVersion: credentialproviderv1alpha1.SchemeGroupVersion, 422 request: &credentialproviderapi.CredentialProviderRequest{ 423 Image: "test.registry.io/foobar", 424 }, 425 expectedData: []byte(`{"kind":"CredentialProviderRequest","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","image":"test.registry.io/foobar"} 426 `), 427 expectedErr: false, 428 }, 429 { 430 name: "successful with v1beta1", 431 apiVersion: credentialproviderv1beta1.SchemeGroupVersion, 432 request: &credentialproviderapi.CredentialProviderRequest{ 433 Image: "test.registry.io/foobar", 434 }, 435 expectedData: []byte(`{"kind":"CredentialProviderRequest","apiVersion":"credentialprovider.kubelet.k8s.io/v1beta1","image":"test.registry.io/foobar"} 436 `), 437 expectedErr: false, 438 }, 439 { 440 name: "successful with v1", 441 apiVersion: credentialproviderv1.SchemeGroupVersion, 442 request: &credentialproviderapi.CredentialProviderRequest{ 443 Image: "test.registry.io/foobar", 444 }, 445 expectedData: []byte(`{"kind":"CredentialProviderRequest","apiVersion":"credentialprovider.kubelet.k8s.io/v1","image":"test.registry.io/foobar"} 446 `), 447 expectedErr: false, 448 }, 449 } 450 451 for _, testcase := range testcases { 452 t.Run(testcase.name, func(t *testing.T) { 453 mediaType := "application/json" 454 info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), mediaType) 455 if !ok { 456 t.Fatalf("unsupported media type: %s", mediaType) 457 } 458 459 e := &execPlugin{ 460 encoder: codecs.EncoderForVersion(info.Serializer, testcase.apiVersion), 461 } 462 463 data, err := e.encodeRequest(testcase.request) 464 if err != nil && !testcase.expectedErr { 465 t.Fatalf("unexpected error: %v", err) 466 } 467 468 if err == nil && testcase.expectedErr { 469 t.Fatalf("expected error %v but got nil", testcase.expectedErr) 470 } 471 472 if !reflect.DeepEqual(data, testcase.expectedData) { 473 t.Errorf("actual encoded data: %v", string(data)) 474 t.Errorf("expected encoded data: %v", string(testcase.expectedData)) 475 t.Errorf("unexpected encoded response") 476 } 477 }) 478 } 479 } 480 481 func Test_decodeResponse(t *testing.T) { 482 testcases := []struct { 483 name string 484 data []byte 485 expectedResponse *credentialproviderapi.CredentialProviderResponse 486 expectedErr bool 487 }{ 488 { 489 name: "success with v1", 490 data: []byte(`{"kind":"CredentialProviderResponse","apiVersion":"credentialprovider.kubelet.k8s.io/v1","cacheKeyType":"Registry","cacheDuration":"1m","auth":{"*.registry.io":{"username":"user","password":"password"}}}`), 491 expectedResponse: &credentialproviderapi.CredentialProviderResponse{ 492 CacheKeyType: credentialproviderapi.RegistryPluginCacheKeyType, 493 CacheDuration: &metav1.Duration{ 494 Duration: time.Minute, 495 }, 496 Auth: map[string]credentialproviderapi.AuthConfig{ 497 "*.registry.io": { 498 Username: "user", 499 Password: "password", 500 }, 501 }, 502 }, 503 expectedErr: false, 504 }, 505 { 506 name: "success with v1beta1", 507 data: []byte(`{"kind":"CredentialProviderResponse","apiVersion":"credentialprovider.kubelet.k8s.io/v1beta1","cacheKeyType":"Registry","cacheDuration":"1m","auth":{"*.registry.io":{"username":"user","password":"password"}}}`), 508 expectedResponse: &credentialproviderapi.CredentialProviderResponse{ 509 CacheKeyType: credentialproviderapi.RegistryPluginCacheKeyType, 510 CacheDuration: &metav1.Duration{ 511 Duration: time.Minute, 512 }, 513 Auth: map[string]credentialproviderapi.AuthConfig{ 514 "*.registry.io": { 515 Username: "user", 516 Password: "password", 517 }, 518 }, 519 }, 520 expectedErr: false, 521 }, 522 { 523 name: "success with v1alpha1", 524 data: []byte(`{"kind":"CredentialProviderResponse","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","cacheKeyType":"Registry","cacheDuration":"1m","auth":{"*.registry.io":{"username":"user","password":"password"}}}`), 525 expectedResponse: &credentialproviderapi.CredentialProviderResponse{ 526 CacheKeyType: credentialproviderapi.RegistryPluginCacheKeyType, 527 CacheDuration: &metav1.Duration{ 528 Duration: time.Minute, 529 }, 530 Auth: map[string]credentialproviderapi.AuthConfig{ 531 "*.registry.io": { 532 Username: "user", 533 Password: "password", 534 }, 535 }, 536 }, 537 expectedErr: false, 538 }, 539 { 540 name: "wrong Kind", 541 data: []byte(`{"kind":"WrongKind","apiVersion":"credentialprovider.kubelet.k8s.io/v1beta1","cacheKeyType":"Registry","cacheDuration":"1m","auth":{"*.registry.io":{"username":"user","password":"password"}}}`), 542 expectedResponse: nil, 543 expectedErr: true, 544 }, 545 { 546 name: "wrong Group", 547 data: []byte(`{"kind":"CredentialProviderResponse","apiVersion":"foobar.kubelet.k8s.io/v1beta1","cacheKeyType":"Registry","cacheDuration":"1m","auth":{"*.registry.io":{"username":"user","password":"password"}}}`), 548 expectedResponse: nil, 549 expectedErr: true, 550 }, 551 } 552 553 for _, testcase := range testcases { 554 t.Run(testcase.name, func(t *testing.T) { 555 e := &execPlugin{} 556 557 decodedResponse, err := e.decodeResponse(testcase.data) 558 if err != nil && !testcase.expectedErr { 559 t.Fatalf("unexpected error: %v", err) 560 } 561 562 if err == nil && testcase.expectedErr { 563 t.Fatalf("expected error %v but not nil", testcase.expectedErr) 564 } 565 566 if !reflect.DeepEqual(decodedResponse, testcase.expectedResponse) { 567 t.Logf("actual decoded response: %#v", decodedResponse) 568 t.Logf("expected decoded response: %#v", testcase.expectedResponse) 569 t.Errorf("unexpected decoded response") 570 } 571 }) 572 } 573 } 574 575 func Test_RegistryCacheKeyType(t *testing.T) { 576 tclock := clock.RealClock{} 577 pluginProvider := &pluginProvider{ 578 clock: tclock, 579 lastCachePurge: tclock.Now(), 580 matchImages: []string{"*.registry.io"}, 581 cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{clock: tclock}), 582 plugin: &fakeExecPlugin{ 583 cacheKeyType: credentialproviderapi.RegistryPluginCacheKeyType, 584 cacheDuration: time.Hour, 585 auth: map[string]credentialproviderapi.AuthConfig{ 586 "*.registry.io": { 587 Username: "user", 588 Password: "password", 589 }, 590 }, 591 }, 592 } 593 594 expectedDockerConfig := credentialprovider.DockerConfig{ 595 "*.registry.io": credentialprovider.DockerConfigEntry{ 596 Username: "user", 597 Password: "password", 598 }, 599 } 600 601 dockerConfig := pluginProvider.Provide("test.registry.io/foo/bar") 602 if !reflect.DeepEqual(dockerConfig, expectedDockerConfig) { 603 t.Logf("actual docker config: %v", dockerConfig) 604 t.Logf("expected docker config: %v", expectedDockerConfig) 605 t.Fatal("unexpected docker config") 606 } 607 608 expectedCacheKeys := []string{"test.registry.io"} 609 cacheKeys := pluginProvider.cache.ListKeys() 610 611 if !reflect.DeepEqual(cacheKeys, expectedCacheKeys) { 612 t.Logf("actual cache keys: %v", cacheKeys) 613 t.Logf("expected cache keys: %v", expectedCacheKeys) 614 t.Error("unexpected cache keys") 615 } 616 617 // nil out the exec plugin, this will test whether credentialproviderapi are fetched 618 // from cache, otherwise Provider should panic 619 pluginProvider.plugin = nil 620 dockerConfig = pluginProvider.Provide("test.registry.io/foo/bar") 621 if !reflect.DeepEqual(dockerConfig, expectedDockerConfig) { 622 t.Logf("actual docker config: %v", dockerConfig) 623 t.Logf("expected docker config: %v", expectedDockerConfig) 624 t.Fatal("unexpected docker config") 625 } 626 } 627 628 func Test_ImageCacheKeyType(t *testing.T) { 629 tclock := clock.RealClock{} 630 pluginProvider := &pluginProvider{ 631 clock: tclock, 632 lastCachePurge: tclock.Now(), 633 matchImages: []string{"*.registry.io"}, 634 cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{clock: tclock}), 635 plugin: &fakeExecPlugin{ 636 cacheKeyType: credentialproviderapi.ImagePluginCacheKeyType, 637 cacheDuration: time.Hour, 638 auth: map[string]credentialproviderapi.AuthConfig{ 639 "*.registry.io": { 640 Username: "user", 641 Password: "password", 642 }, 643 }, 644 }, 645 } 646 647 expectedDockerConfig := credentialprovider.DockerConfig{ 648 "*.registry.io": credentialprovider.DockerConfigEntry{ 649 Username: "user", 650 Password: "password", 651 }, 652 } 653 654 dockerConfig := pluginProvider.Provide("test.registry.io/foo/bar") 655 if !reflect.DeepEqual(dockerConfig, expectedDockerConfig) { 656 t.Logf("actual docker config: %v", dockerConfig) 657 t.Logf("expected docker config: %v", expectedDockerConfig) 658 t.Fatal("unexpected docker config") 659 } 660 661 expectedCacheKeys := []string{"test.registry.io/foo/bar"} 662 cacheKeys := pluginProvider.cache.ListKeys() 663 664 if !reflect.DeepEqual(cacheKeys, expectedCacheKeys) { 665 t.Logf("actual cache keys: %v", cacheKeys) 666 t.Logf("expected cache keys: %v", expectedCacheKeys) 667 t.Error("unexpected cache keys") 668 } 669 670 // nil out the exec plugin, this will test whether credentialproviderapi are fetched 671 // from cache, otherwise Provider should panic 672 pluginProvider.plugin = nil 673 dockerConfig = pluginProvider.Provide("test.registry.io/foo/bar") 674 if !reflect.DeepEqual(dockerConfig, expectedDockerConfig) { 675 t.Logf("actual docker config: %v", dockerConfig) 676 t.Logf("expected docker config: %v", expectedDockerConfig) 677 t.Fatal("unexpected docker config") 678 } 679 } 680 681 func Test_GlobalCacheKeyType(t *testing.T) { 682 tclock := clock.RealClock{} 683 pluginProvider := &pluginProvider{ 684 clock: tclock, 685 lastCachePurge: tclock.Now(), 686 matchImages: []string{"*.registry.io"}, 687 cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{clock: tclock}), 688 plugin: &fakeExecPlugin{ 689 cacheKeyType: credentialproviderapi.GlobalPluginCacheKeyType, 690 cacheDuration: time.Hour, 691 auth: map[string]credentialproviderapi.AuthConfig{ 692 "*.registry.io": { 693 Username: "user", 694 Password: "password", 695 }, 696 }, 697 }, 698 } 699 700 expectedDockerConfig := credentialprovider.DockerConfig{ 701 "*.registry.io": credentialprovider.DockerConfigEntry{ 702 Username: "user", 703 Password: "password", 704 }, 705 } 706 707 dockerConfig := pluginProvider.Provide("test.registry.io/foo/bar") 708 if !reflect.DeepEqual(dockerConfig, expectedDockerConfig) { 709 t.Logf("actual docker config: %v", dockerConfig) 710 t.Logf("expected docker config: %v", expectedDockerConfig) 711 t.Fatal("unexpected docker config") 712 } 713 714 expectedCacheKeys := []string{"global"} 715 cacheKeys := pluginProvider.cache.ListKeys() 716 717 if !reflect.DeepEqual(cacheKeys, expectedCacheKeys) { 718 t.Logf("actual cache keys: %v", cacheKeys) 719 t.Logf("expected cache keys: %v", expectedCacheKeys) 720 t.Error("unexpected cache keys") 721 } 722 723 // nil out the exec plugin, this will test whether credentialproviderapi are fetched 724 // from cache, otherwise Provider should panic 725 pluginProvider.plugin = nil 726 dockerConfig = pluginProvider.Provide("test.registry.io/foo/bar") 727 if !reflect.DeepEqual(dockerConfig, expectedDockerConfig) { 728 t.Logf("actual docker config: %v", dockerConfig) 729 t.Logf("expected docker config: %v", expectedDockerConfig) 730 t.Fatal("unexpected docker config") 731 } 732 } 733 734 func Test_NoCacheResponse(t *testing.T) { 735 tclock := clock.RealClock{} 736 pluginProvider := &pluginProvider{ 737 clock: tclock, 738 lastCachePurge: tclock.Now(), 739 matchImages: []string{"*.registry.io"}, 740 cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{clock: tclock}), 741 plugin: &fakeExecPlugin{ 742 cacheKeyType: credentialproviderapi.GlobalPluginCacheKeyType, 743 cacheDuration: 0, // no cache 744 auth: map[string]credentialproviderapi.AuthConfig{ 745 "*.registry.io": { 746 Username: "user", 747 Password: "password", 748 }, 749 }, 750 }, 751 } 752 753 expectedDockerConfig := credentialprovider.DockerConfig{ 754 "*.registry.io": credentialprovider.DockerConfigEntry{ 755 Username: "user", 756 Password: "password", 757 }, 758 } 759 760 dockerConfig := pluginProvider.Provide("test.registry.io/foo/bar") 761 if !reflect.DeepEqual(dockerConfig, expectedDockerConfig) { 762 t.Logf("actual docker config: %v", dockerConfig) 763 t.Logf("expected docker config: %v", expectedDockerConfig) 764 t.Fatal("unexpected docker config") 765 } 766 767 expectedCacheKeys := []string{} 768 cacheKeys := pluginProvider.cache.ListKeys() 769 if !reflect.DeepEqual(cacheKeys, expectedCacheKeys) { 770 t.Logf("actual cache keys: %v", cacheKeys) 771 t.Logf("expected cache keys: %v", expectedCacheKeys) 772 t.Error("unexpected cache keys") 773 } 774 } 775 776 func Test_ExecPluginEnvVars(t *testing.T) { 777 testcases := []struct { 778 name string 779 systemEnvVars []string 780 execPlugin *execPlugin 781 expectedEnvVars []string 782 }{ 783 { 784 name: "positive append system env vars", 785 systemEnvVars: []string{"HOME=/home/foo", "PATH=/usr/bin"}, 786 execPlugin: &execPlugin{ 787 envVars: []kubeletconfig.ExecEnvVar{ 788 { 789 Name: "SUPER_SECRET_STRONG_ACCESS_KEY", 790 Value: "123456789", 791 }, 792 }, 793 }, 794 expectedEnvVars: []string{ 795 "HOME=/home/foo", 796 "PATH=/usr/bin", 797 "SUPER_SECRET_STRONG_ACCESS_KEY=123456789", 798 }, 799 }, 800 { 801 name: "positive no env vars provided in plugin", 802 systemEnvVars: []string{"HOME=/home/foo", "PATH=/usr/bin"}, 803 execPlugin: &execPlugin{}, 804 expectedEnvVars: []string{ 805 "HOME=/home/foo", 806 "PATH=/usr/bin", 807 }, 808 }, 809 { 810 name: "positive no system env vars but env vars are provided in plugin", 811 execPlugin: &execPlugin{ 812 envVars: []kubeletconfig.ExecEnvVar{ 813 { 814 Name: "SUPER_SECRET_STRONG_ACCESS_KEY", 815 Value: "123456789", 816 }, 817 }, 818 }, 819 expectedEnvVars: []string{ 820 "SUPER_SECRET_STRONG_ACCESS_KEY=123456789", 821 }, 822 }, 823 { 824 name: "positive no system or plugin provided env vars", 825 execPlugin: &execPlugin{}, 826 expectedEnvVars: nil, 827 }, 828 { 829 name: "positive plugin provided vars takes priority", 830 systemEnvVars: []string{"HOME=/home/foo", "PATH=/usr/bin", "SUPER_SECRET_STRONG_ACCESS_KEY=1111"}, 831 execPlugin: &execPlugin{ 832 envVars: []kubeletconfig.ExecEnvVar{ 833 { 834 Name: "SUPER_SECRET_STRONG_ACCESS_KEY", 835 Value: "123456789", 836 }, 837 }, 838 }, 839 expectedEnvVars: []string{ 840 "HOME=/home/foo", 841 "PATH=/usr/bin", 842 "SUPER_SECRET_STRONG_ACCESS_KEY=1111", 843 "SUPER_SECRET_STRONG_ACCESS_KEY=123456789", 844 }, 845 }, 846 } 847 848 for _, testcase := range testcases { 849 t.Run(testcase.name, func(t *testing.T) { 850 testcase.execPlugin.environ = func() []string { 851 return testcase.systemEnvVars 852 } 853 854 var configVars []string 855 for _, envVar := range testcase.execPlugin.envVars { 856 configVars = append(configVars, fmt.Sprintf("%s=%s", envVar.Name, envVar.Value)) 857 } 858 merged := mergeEnvVars(testcase.systemEnvVars, configVars) 859 860 err := validate(testcase.expectedEnvVars, merged) 861 if err != nil { 862 t.Logf("unexpecged error %v", err) 863 } 864 }) 865 } 866 } 867 868 func validate(expected, actual []string) error { 869 if len(actual) != len(expected) { 870 return fmt.Errorf("actual env var length [%d] and expected env var length [%d] don't match", 871 len(actual), len(expected)) 872 } 873 874 for i := range actual { 875 if actual[i] != expected[i] { 876 return fmt.Errorf("mismatch in expected env var %s and actual env var %s", actual[i], expected[i]) 877 } 878 } 879 880 return nil 881 }