1
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
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
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
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
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
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 }
607
View as plain text