|
| 1 | +/* |
| 2 | +Copyright 2025 The Flux 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 cache |
| 18 | + |
| 19 | +import ( |
| 20 | + "sync" |
| 21 | + "time" |
| 22 | +) |
| 23 | + |
| 24 | +// Token is an interface that represents an access token that can be used |
| 25 | +// to authenticate with a cloud provider. The only common method is to get the |
| 26 | +// duration of the token, because different providers may have different ways to |
| 27 | +// represent the token. For example, Azure and GCP use an opaque string token, |
| 28 | +// while AWS uses the pair of access key id and secret access key. Consumers of |
| 29 | +// this token should know what type to cast this interface to. |
| 30 | +type Token interface { |
| 31 | + // GetDuration returns the duration for which the token is valid relative to |
| 32 | + // approximately time.Now(). This is used to determine when the token should |
| 33 | + // be refreshed. |
| 34 | + GetDuration() time.Duration |
| 35 | +} |
| 36 | + |
| 37 | +// TokenCache is a thread-safe cache specialized in storing and retrieving |
| 38 | +// access tokens. It uses an LRU cache as the underlying storage and takes |
| 39 | +// care of expiring tokens in a pessimistic way by storing both a timestamp |
| 40 | +// with a monotonic clock (the Go default) and an absolute timestamp created |
| 41 | +// from the Unix timestamp of when the token was created. The token is |
| 42 | +// considered expired when either timestamps are older than the current time. |
| 43 | +// This strategy ensures that expired tokens aren't kept in the cache for |
| 44 | +// longer than their expiration time. Also, tokens expire on 80% of their |
| 45 | +// lifetime, which is the same strategy used by kubelet for rotating |
| 46 | +// ServiceAccount tokens. |
| 47 | +type TokenCache struct { |
| 48 | + cache *LRU[*tokenItem] |
| 49 | + mu sync.Mutex |
| 50 | +} |
| 51 | + |
| 52 | +type tokenItem struct { |
| 53 | + token Token |
| 54 | + mono time.Time |
| 55 | + unix time.Time |
| 56 | +} |
| 57 | + |
| 58 | +func (ti *tokenItem) expired() bool { |
| 59 | + now := time.Now() |
| 60 | + return ti.mono.Before(now) || ti.unix.Before(now) |
| 61 | +} |
| 62 | + |
| 63 | +// NewTokenCache returns a new TokenCache with the given capacity. |
| 64 | +func NewTokenCache(capacity int, opts ...Options) *TokenCache { |
| 65 | + cache, _ := NewLRU[*tokenItem](capacity, opts...) |
| 66 | + return &TokenCache{cache: cache} |
| 67 | +} |
| 68 | + |
| 69 | +// Get returns the token for the given key, or nil if the key is not in the cache. |
| 70 | +func (c *TokenCache) Get(key string) Token { |
| 71 | + c.mu.Lock() |
| 72 | + defer c.mu.Unlock() |
| 73 | + |
| 74 | + item, err := c.cache.Get(key) |
| 75 | + if err != nil { |
| 76 | + return nil |
| 77 | + } |
| 78 | + |
| 79 | + if item.expired() { |
| 80 | + c.cache.Delete(key) |
| 81 | + return nil |
| 82 | + } |
| 83 | + |
| 84 | + return item.token |
| 85 | +} |
| 86 | + |
| 87 | +// Set adds a token to the cache with the given key. |
| 88 | +func (c *TokenCache) Set(key string, token Token) { |
| 89 | + item := c.newTokenItem(token) |
| 90 | + c.mu.Lock() |
| 91 | + c.cache.Set(key, item) |
| 92 | + c.mu.Unlock() |
| 93 | +} |
| 94 | + |
| 95 | +// RecordCacheEvent records a cache event (cache_miss or cache_hit) with kind, |
| 96 | +// name and namespace of the associated object being reconciled. |
| 97 | +func (c *TokenCache) RecordCacheEvent(event, kind, name, namespace string) { |
| 98 | + c.cache.RecordCacheEvent(event, kind, name, namespace) |
| 99 | +} |
| 100 | + |
| 101 | +// DeleteCacheEvent deletes the cache event (cache_miss or cache_hit) metric for |
| 102 | +// the associated object being reconciled, given their kind, name and namespace. |
| 103 | +func (c *TokenCache) DeleteCacheEvent(event, kind, name, namespace string) { |
| 104 | + c.cache.DeleteCacheEvent(event, kind, name, namespace) |
| 105 | +} |
| 106 | + |
| 107 | +func (c *TokenCache) newTokenItem(token Token) *tokenItem { |
| 108 | + // Kubelet rotates ServiceAccount tokens when 80% of their lifetime has |
| 109 | + // passed, so we'll use the same threshold to consider tokens expired. |
| 110 | + // |
| 111 | + // Ref: https://github.com/kubernetes/kubernetes/blob/4032177faf21ae2f99a2012634167def2376b370/pkg/kubelet/token/token_manager.go#L172-L174 |
| 112 | + d := (token.GetDuration() * 8) / 10 |
| 113 | + |
| 114 | + mono := time.Now().Add(d) |
| 115 | + unix := time.Unix(mono.Unix(), 0) |
| 116 | + |
| 117 | + return &tokenItem{ |
| 118 | + token: token, |
| 119 | + mono: mono, |
| 120 | + unix: unix, |
| 121 | + } |
| 122 | +} |
0 commit comments