k8s.io/kubernetes@v1.29.3/pkg/kubelet/token/token_manager_test.go (about) 1 /* 2 Copyright 2018 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 token 18 19 import ( 20 "fmt" 21 "testing" 22 "time" 23 24 authenticationv1 "k8s.io/api/authentication/v1" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/types" 27 testingclock "k8s.io/utils/clock/testing" 28 ) 29 30 func TestTokenCachingAndExpiration(t *testing.T) { 31 type suite struct { 32 clock *testingclock.FakeClock 33 tg *fakeTokenGetter 34 mgr *Manager 35 } 36 37 cases := []struct { 38 name string 39 exp time.Duration 40 f func(t *testing.T, s *suite) 41 }{ 42 { 43 name: "rotate hour token expires in the last 12 minutes", 44 exp: time.Hour, 45 f: func(t *testing.T, s *suite) { 46 s.clock.SetTime(s.clock.Now().Add(50 * time.Minute)) 47 if _, err := s.mgr.GetServiceAccountToken("a", "b", getTokenRequest()); err != nil { 48 t.Fatalf("unexpected error: %v", err) 49 } 50 if s.tg.count != 2 { 51 t.Fatalf("expected token to be refreshed: call count was %d", s.tg.count) 52 } 53 }, 54 }, 55 { 56 name: "rotate 24 hour token that expires in 40 hours", 57 exp: 40 * time.Hour, 58 f: func(t *testing.T, s *suite) { 59 s.clock.SetTime(s.clock.Now().Add(25 * time.Hour)) 60 if _, err := s.mgr.GetServiceAccountToken("a", "b", getTokenRequest()); err != nil { 61 t.Fatalf("unexpected error: %v", err) 62 } 63 if s.tg.count != 2 { 64 t.Fatalf("expected token to be refreshed: call count was %d", s.tg.count) 65 } 66 }, 67 }, 68 { 69 name: "rotate hour token fails, old token is still valid, doesn't error", 70 exp: time.Hour, 71 f: func(t *testing.T, s *suite) { 72 s.clock.SetTime(s.clock.Now().Add(50 * time.Minute)) 73 tg := &fakeTokenGetter{ 74 err: fmt.Errorf("err"), 75 } 76 s.mgr.getToken = tg.getToken 77 tr, err := s.mgr.GetServiceAccountToken("a", "b", getTokenRequest()) 78 if err != nil { 79 t.Fatalf("unexpected error: %v", err) 80 } 81 if tr.Status.Token != "foo" { 82 t.Fatalf("unexpected token: %v", tr.Status.Token) 83 } 84 }, 85 }, 86 } 87 88 for _, c := range cases { 89 t.Run(c.name, func(t *testing.T) { 90 clock := testingclock.NewFakeClock(time.Time{}.Add(30 * 24 * time.Hour)) 91 expSecs := int64(c.exp.Seconds()) 92 s := &suite{ 93 clock: clock, 94 mgr: NewManager(nil), 95 tg: &fakeTokenGetter{ 96 tr: &authenticationv1.TokenRequest{ 97 Spec: authenticationv1.TokenRequestSpec{ 98 ExpirationSeconds: &expSecs, 99 }, 100 Status: authenticationv1.TokenRequestStatus{ 101 Token: "foo", 102 ExpirationTimestamp: metav1.Time{Time: clock.Now().Add(c.exp)}, 103 }, 104 }, 105 }, 106 } 107 s.mgr.getToken = s.tg.getToken 108 s.mgr.clock = s.clock 109 if _, err := s.mgr.GetServiceAccountToken("a", "b", getTokenRequest()); err != nil { 110 t.Fatalf("unexpected error: %v", err) 111 } 112 if s.tg.count != 1 { 113 t.Fatalf("unexpected client call, got: %d, want: 1", s.tg.count) 114 } 115 116 if _, err := s.mgr.GetServiceAccountToken("a", "b", getTokenRequest()); err != nil { 117 t.Fatalf("unexpected error: %v", err) 118 } 119 if s.tg.count != 1 { 120 t.Fatalf("expected token to be served from cache: saw %d", s.tg.count) 121 } 122 123 c.f(t, s) 124 }) 125 } 126 } 127 128 func TestRequiresRefresh(t *testing.T) { 129 start := time.Now() 130 cases := []struct { 131 now, exp time.Time 132 expectRefresh bool 133 requestTweaks func(*authenticationv1.TokenRequest) 134 }{ 135 { 136 now: start.Add(10 * time.Minute), 137 exp: start.Add(60 * time.Minute), 138 expectRefresh: false, 139 }, 140 { 141 now: start.Add(50 * time.Minute), 142 exp: start.Add(60 * time.Minute), 143 expectRefresh: true, 144 }, 145 { 146 now: start.Add(25 * time.Hour), 147 exp: start.Add(60 * time.Hour), 148 expectRefresh: true, 149 }, 150 { 151 now: start.Add(70 * time.Minute), 152 exp: start.Add(60 * time.Minute), 153 expectRefresh: true, 154 }, 155 { 156 // expiry will be overwritten by the tweak below. 157 now: start.Add(0 * time.Minute), 158 exp: start.Add(60 * time.Minute), 159 expectRefresh: false, 160 requestTweaks: func(tr *authenticationv1.TokenRequest) { 161 tr.Spec.ExpirationSeconds = nil 162 }, 163 }, 164 } 165 166 for i, c := range cases { 167 t.Run(fmt.Sprint(i), func(t *testing.T) { 168 clock := testingclock.NewFakeClock(c.now) 169 secs := int64(c.exp.Sub(start).Seconds()) 170 tr := &authenticationv1.TokenRequest{ 171 Spec: authenticationv1.TokenRequestSpec{ 172 ExpirationSeconds: &secs, 173 }, 174 Status: authenticationv1.TokenRequestStatus{ 175 ExpirationTimestamp: metav1.Time{Time: c.exp}, 176 }, 177 } 178 179 if c.requestTweaks != nil { 180 c.requestTweaks(tr) 181 } 182 183 mgr := NewManager(nil) 184 mgr.clock = clock 185 186 rr := mgr.requiresRefresh(tr) 187 if rr != c.expectRefresh { 188 t.Fatalf("unexpected requiresRefresh result, got: %v, want: %v", rr, c.expectRefresh) 189 } 190 }) 191 } 192 } 193 194 func TestDeleteServiceAccountToken(t *testing.T) { 195 type request struct { 196 name, namespace string 197 tr authenticationv1.TokenRequest 198 shouldFail bool 199 } 200 201 cases := []struct { 202 name string 203 requestIndex []int 204 deletePodUID []types.UID 205 expLeftIndex []int 206 }{ 207 { 208 name: "delete none with all success requests", 209 requestIndex: []int{0, 1, 2}, 210 expLeftIndex: []int{0, 1, 2}, 211 }, 212 { 213 name: "delete one with all success requests", 214 requestIndex: []int{0, 1, 2}, 215 deletePodUID: []types.UID{"fake-uid-1"}, 216 expLeftIndex: []int{1, 2}, 217 }, 218 { 219 name: "delete two with all success requests", 220 requestIndex: []int{0, 1, 2}, 221 deletePodUID: []types.UID{"fake-uid-1", "fake-uid-3"}, 222 expLeftIndex: []int{1}, 223 }, 224 { 225 name: "delete all with all success requests", 226 requestIndex: []int{0, 1, 2}, 227 deletePodUID: []types.UID{"fake-uid-1", "fake-uid-2", "fake-uid-3"}, 228 }, 229 { 230 name: "delete no pod with failed requests", 231 requestIndex: []int{0, 1, 2, 3}, 232 deletePodUID: []types.UID{}, 233 expLeftIndex: []int{0, 1, 2}, 234 }, 235 { 236 name: "delete other pod with failed requests", 237 requestIndex: []int{0, 1, 2, 3}, 238 deletePodUID: []types.UID{"fake-uid-2"}, 239 expLeftIndex: []int{0, 2}, 240 }, 241 { 242 name: "delete no pod with request which success after failure", 243 requestIndex: []int{0, 1, 2, 3, 4}, 244 deletePodUID: []types.UID{}, 245 expLeftIndex: []int{0, 1, 2, 4}, 246 }, 247 { 248 name: "delete the pod which success after failure", 249 requestIndex: []int{0, 1, 2, 3, 4}, 250 deletePodUID: []types.UID{"fake-uid-4"}, 251 expLeftIndex: []int{0, 1, 2}, 252 }, 253 { 254 name: "delete other pod with request which success after failure", 255 requestIndex: []int{0, 1, 2, 3, 4}, 256 deletePodUID: []types.UID{"fake-uid-1"}, 257 expLeftIndex: []int{1, 2, 4}, 258 }, 259 { 260 name: "delete some pod not in the set", 261 requestIndex: []int{0, 1, 2}, 262 deletePodUID: []types.UID{"fake-uid-100", "fake-uid-200"}, 263 expLeftIndex: []int{0, 1, 2}, 264 }, 265 } 266 267 for _, c := range cases { 268 t.Run(c.name, func(t *testing.T) { 269 requests := []request{ 270 { 271 name: "fake-name-1", 272 namespace: "fake-namespace-1", 273 tr: authenticationv1.TokenRequest{ 274 Spec: authenticationv1.TokenRequestSpec{ 275 BoundObjectRef: &authenticationv1.BoundObjectReference{ 276 UID: "fake-uid-1", 277 Name: "fake-name-1", 278 }, 279 }, 280 }, 281 shouldFail: false, 282 }, 283 { 284 name: "fake-name-2", 285 namespace: "fake-namespace-2", 286 tr: authenticationv1.TokenRequest{ 287 Spec: authenticationv1.TokenRequestSpec{ 288 BoundObjectRef: &authenticationv1.BoundObjectReference{ 289 UID: "fake-uid-2", 290 Name: "fake-name-2", 291 }, 292 }, 293 }, 294 shouldFail: false, 295 }, 296 { 297 name: "fake-name-3", 298 namespace: "fake-namespace-3", 299 tr: authenticationv1.TokenRequest{ 300 Spec: authenticationv1.TokenRequestSpec{ 301 BoundObjectRef: &authenticationv1.BoundObjectReference{ 302 UID: "fake-uid-3", 303 Name: "fake-name-3", 304 }, 305 }, 306 }, 307 shouldFail: false, 308 }, 309 { 310 name: "fake-name-4", 311 namespace: "fake-namespace-4", 312 tr: authenticationv1.TokenRequest{ 313 Spec: authenticationv1.TokenRequestSpec{ 314 BoundObjectRef: &authenticationv1.BoundObjectReference{ 315 UID: "fake-uid-4", 316 Name: "fake-name-4", 317 }, 318 }, 319 }, 320 shouldFail: true, 321 }, 322 { 323 //exactly the same with last one, besides it will success 324 name: "fake-name-4", 325 namespace: "fake-namespace-4", 326 tr: authenticationv1.TokenRequest{ 327 Spec: authenticationv1.TokenRequestSpec{ 328 BoundObjectRef: &authenticationv1.BoundObjectReference{ 329 UID: "fake-uid-4", 330 Name: "fake-name-4", 331 }, 332 }, 333 }, 334 shouldFail: false, 335 }, 336 } 337 testMgr := NewManager(nil) 338 testMgr.clock = testingclock.NewFakeClock(time.Time{}.Add(30 * 24 * time.Hour)) 339 340 successGetToken := func(_, _ string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { 341 tr.Status = authenticationv1.TokenRequestStatus{ 342 ExpirationTimestamp: metav1.Time{Time: testMgr.clock.Now().Add(10 * time.Hour)}, 343 } 344 return tr, nil 345 } 346 failGetToken := func(_, _ string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { 347 return nil, fmt.Errorf("fail tr") 348 } 349 350 for _, index := range c.requestIndex { 351 req := requests[index] 352 if req.shouldFail { 353 testMgr.getToken = failGetToken 354 } else { 355 testMgr.getToken = successGetToken 356 } 357 testMgr.GetServiceAccountToken(req.namespace, req.name, &req.tr) 358 } 359 360 for _, uid := range c.deletePodUID { 361 testMgr.DeleteServiceAccountToken(uid) 362 } 363 if len(c.expLeftIndex) != len(testMgr.cache) { 364 t.Errorf("%s got unexpected result: expected left cache size is %d, got %d", c.name, len(c.expLeftIndex), len(testMgr.cache)) 365 } 366 for _, leftIndex := range c.expLeftIndex { 367 r := requests[leftIndex] 368 _, ok := testMgr.get(keyFunc(r.name, r.namespace, &r.tr)) 369 if !ok { 370 t.Errorf("%s got unexpected result: expected token request %v exist in cache, but not", c.name, r) 371 } 372 } 373 }) 374 } 375 } 376 377 type fakeTokenGetter struct { 378 count int 379 tr *authenticationv1.TokenRequest 380 err error 381 } 382 383 func (ftg *fakeTokenGetter) getToken(name, namespace string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { 384 ftg.count++ 385 return ftg.tr, ftg.err 386 } 387 388 func TestCleanup(t *testing.T) { 389 cases := []struct { 390 name string 391 relativeExp time.Duration 392 expectedCacheSize int 393 }{ 394 { 395 name: "don't cleanup unexpired tokens", 396 relativeExp: -1 * time.Hour, 397 expectedCacheSize: 0, 398 }, 399 { 400 name: "cleanup expired tokens", 401 relativeExp: time.Hour, 402 expectedCacheSize: 1, 403 }, 404 } 405 for _, c := range cases { 406 t.Run(c.name, func(t *testing.T) { 407 clock := testingclock.NewFakeClock(time.Time{}.Add(24 * time.Hour)) 408 mgr := NewManager(nil) 409 mgr.clock = clock 410 411 mgr.set("key", &authenticationv1.TokenRequest{ 412 Status: authenticationv1.TokenRequestStatus{ 413 ExpirationTimestamp: metav1.Time{Time: mgr.clock.Now().Add(c.relativeExp)}, 414 }, 415 }) 416 mgr.cleanup() 417 if got, want := len(mgr.cache), c.expectedCacheSize; got != want { 418 t.Fatalf("unexpected number of cache entries after cleanup, got: %d, want: %d", got, want) 419 } 420 }) 421 } 422 } 423 424 func TestKeyFunc(t *testing.T) { 425 type tokenRequestUnit struct { 426 name string 427 namespace string 428 tr *authenticationv1.TokenRequest 429 } 430 getKeyFunc := func(u tokenRequestUnit) string { 431 return keyFunc(u.name, u.namespace, u.tr) 432 } 433 434 cases := []struct { 435 name string 436 trus []tokenRequestUnit 437 target tokenRequestUnit 438 439 shouldHit bool 440 }{ 441 { 442 name: "hit", 443 trus: []tokenRequestUnit{ 444 { 445 name: "foo-sa", 446 namespace: "foo-ns", 447 tr: &authenticationv1.TokenRequest{ 448 Spec: authenticationv1.TokenRequestSpec{ 449 Audiences: []string{"foo1", "foo2"}, 450 ExpirationSeconds: getInt64Point(2000), 451 BoundObjectRef: &authenticationv1.BoundObjectReference{ 452 Kind: "pod", 453 Name: "foo-pod", 454 UID: "foo-uid", 455 }, 456 }, 457 }, 458 }, 459 { 460 name: "ame-sa", 461 namespace: "ame-ns", 462 tr: &authenticationv1.TokenRequest{ 463 Spec: authenticationv1.TokenRequestSpec{ 464 Audiences: []string{"ame1", "ame2"}, 465 ExpirationSeconds: getInt64Point(2000), 466 BoundObjectRef: &authenticationv1.BoundObjectReference{ 467 Kind: "pod", 468 Name: "ame-pod", 469 UID: "ame-uid", 470 }, 471 }, 472 }, 473 }, 474 }, 475 target: tokenRequestUnit{ 476 name: "foo-sa", 477 namespace: "foo-ns", 478 tr: &authenticationv1.TokenRequest{ 479 Spec: authenticationv1.TokenRequestSpec{ 480 Audiences: []string{"foo1", "foo2"}, 481 ExpirationSeconds: getInt64Point(2000), 482 BoundObjectRef: &authenticationv1.BoundObjectReference{ 483 Kind: "pod", 484 Name: "foo-pod", 485 UID: "foo-uid", 486 }, 487 }, 488 }, 489 }, 490 shouldHit: true, 491 }, 492 { 493 name: "not hit due to different ExpirationSeconds", 494 trus: []tokenRequestUnit{ 495 { 496 name: "foo-sa", 497 namespace: "foo-ns", 498 tr: &authenticationv1.TokenRequest{ 499 Spec: authenticationv1.TokenRequestSpec{ 500 Audiences: []string{"foo1", "foo2"}, 501 ExpirationSeconds: getInt64Point(2000), 502 BoundObjectRef: &authenticationv1.BoundObjectReference{ 503 Kind: "pod", 504 Name: "foo-pod", 505 UID: "foo-uid", 506 }, 507 }, 508 }, 509 }, 510 }, 511 target: tokenRequestUnit{ 512 name: "foo-sa", 513 namespace: "foo-ns", 514 tr: &authenticationv1.TokenRequest{ 515 Spec: authenticationv1.TokenRequestSpec{ 516 Audiences: []string{"foo1", "foo2"}, 517 //everthing is same besides ExpirationSeconds 518 ExpirationSeconds: getInt64Point(2001), 519 BoundObjectRef: &authenticationv1.BoundObjectReference{ 520 Kind: "pod", 521 Name: "foo-pod", 522 UID: "foo-uid", 523 }, 524 }, 525 }, 526 }, 527 shouldHit: false, 528 }, 529 { 530 name: "not hit due to different BoundObjectRef", 531 trus: []tokenRequestUnit{ 532 { 533 name: "foo-sa", 534 namespace: "foo-ns", 535 tr: &authenticationv1.TokenRequest{ 536 Spec: authenticationv1.TokenRequestSpec{ 537 Audiences: []string{"foo1", "foo2"}, 538 ExpirationSeconds: getInt64Point(2000), 539 BoundObjectRef: &authenticationv1.BoundObjectReference{ 540 Kind: "pod", 541 Name: "foo-pod", 542 UID: "foo-uid", 543 }, 544 }, 545 }, 546 }, 547 }, 548 target: tokenRequestUnit{ 549 name: "foo-sa", 550 namespace: "foo-ns", 551 tr: &authenticationv1.TokenRequest{ 552 Spec: authenticationv1.TokenRequestSpec{ 553 Audiences: []string{"foo1", "foo2"}, 554 ExpirationSeconds: getInt64Point(2000), 555 BoundObjectRef: &authenticationv1.BoundObjectReference{ 556 Kind: "pod", 557 //everthing is same besides BoundObjectRef.Name 558 Name: "diff-pod", 559 UID: "foo-uid", 560 }, 561 }, 562 }, 563 }, 564 shouldHit: false, 565 }, 566 } 567 568 for _, c := range cases { 569 t.Run(c.name, func(t *testing.T) { 570 mgr := NewManager(nil) 571 mgr.clock = testingclock.NewFakeClock(time.Time{}.Add(30 * 24 * time.Hour)) 572 for _, tru := range c.trus { 573 mgr.set(getKeyFunc(tru), &authenticationv1.TokenRequest{ 574 Status: authenticationv1.TokenRequestStatus{ 575 //make sure the token cache would not be cleaned by token manager clenaup func 576 ExpirationTimestamp: metav1.Time{Time: mgr.clock.Now().Add(50 * time.Minute)}, 577 }, 578 }) 579 } 580 _, hit := mgr.get(getKeyFunc(c.target)) 581 582 if hit != c.shouldHit { 583 t.Errorf("%s got unexpected hit result: expected to be %t, got %t", c.name, c.shouldHit, hit) 584 } 585 }) 586 } 587 588 } 589 590 func getTokenRequest() *authenticationv1.TokenRequest { 591 return &authenticationv1.TokenRequest{ 592 Spec: authenticationv1.TokenRequestSpec{ 593 Audiences: []string{"foo1", "foo2"}, 594 ExpirationSeconds: getInt64Point(2000), 595 BoundObjectRef: &authenticationv1.BoundObjectReference{ 596 Kind: "pod", 597 Name: "foo-pod", 598 UID: "foo-uid", 599 }, 600 }, 601 } 602 } 603 604 func getInt64Point(v int64) *int64 { 605 return &v 606 }