|
| 1 | +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one |
| 2 | +// or more contributor license agreements. Licensed under the Elastic License 2.0; |
| 3 | +// you may not use this file except in compliance with the Elastic License 2.0. |
| 4 | + |
| 5 | +package kubernetessecrets |
| 6 | + |
| 7 | +import ( |
| 8 | + "sync" |
| 9 | + "time" |
| 10 | +) |
| 11 | + |
| 12 | +// expirationCache is a store that expires items after time.Now - secret.lastAccess > ttl (if ttl > 0) at Get or List. |
| 13 | +// expirationCache works with *cacheEntry, a pointer struct that wraps secret, instead of secret directly because map |
| 14 | +// structure in standard go library never removes the buckets from memory even after removing all the elements from it. |
| 15 | +// However, since *cacheEntry is a pointer it can be garbage collected when no longer referenced by the GC, such as |
| 16 | +// when deleted from the map. More importantly working with a pointer makes the entry in the map bucket, that doesn't |
| 17 | +// get deallocated, to utilise only 8 bytes on a 64-bit system. |
| 18 | +type expirationCache struct { |
| 19 | + sync.Mutex |
| 20 | + // ttl is the time-to-live for items in the cache |
| 21 | + ttl time.Duration |
| 22 | + // items is the underlying cache store. |
| 23 | + items map[string]*cacheEntry |
| 24 | +} |
| 25 | + |
| 26 | +type cacheEntry struct { |
| 27 | + s secret |
| 28 | + lastAccess time.Time |
| 29 | +} |
| 30 | + |
| 31 | +// Get returns the secret associated with the given key from the store if it exists and is not expired. If updateAccess is true |
| 32 | +// and the secret exists, essentially the expiration check is skipped and the lastAccess timestamp is updated to time.Now(). |
| 33 | +func (c *expirationCache) Get(key string, updateAccess bool) (secret, bool) { |
| 34 | + c.Lock() |
| 35 | + defer c.Unlock() |
| 36 | + |
| 37 | + entry, exists := c.items[key] |
| 38 | + if !exists { |
| 39 | + return secret{}, false |
| 40 | + } |
| 41 | + if updateAccess { |
| 42 | + entry.lastAccess = time.Now() |
| 43 | + } else if c.isExpired(entry.lastAccess) { |
| 44 | + delete(c.items, key) |
| 45 | + return secret{}, false |
| 46 | + } |
| 47 | + |
| 48 | + return entry.s, true |
| 49 | +} |
| 50 | + |
| 51 | +// AddConditionally adds the given secret to the store if the given condition returns true. If there is no existing |
| 52 | +// secret, the condition will be called with an empty secret and false. If updateAccess is true and the secret already exists, |
| 53 | +// then the lastAccess timestamp is updated to time.Now() independently of the condition result. |
| 54 | +// Note: if the given condition is nil, then it is considered as a condition that always returns false. |
| 55 | +func (c *expirationCache) AddConditionally(key string, in secret, updateAccess bool, condition conditionFn) { |
| 56 | + c.Lock() |
| 57 | + defer c.Unlock() |
| 58 | + entry, exists := c.items[key] |
| 59 | + if !exists { |
| 60 | + if condition != nil && condition(secret{}, false) { |
| 61 | + c.items[key] = &cacheEntry{in, time.Now()} |
| 62 | + } |
| 63 | + return |
| 64 | + } |
| 65 | + |
| 66 | + if condition != nil && condition(entry.s, true) { |
| 67 | + entry.s = in |
| 68 | + entry.lastAccess = time.Now() |
| 69 | + } else if updateAccess { |
| 70 | + entry.lastAccess = time.Now() |
| 71 | + } |
| 72 | +} |
| 73 | + |
| 74 | +// isExpired returns true if the item has expired based on the ttl |
| 75 | +func (c *expirationCache) isExpired(lastAccess time.Time) bool { |
| 76 | + if c.ttl <= 0 { |
| 77 | + // no expiration |
| 78 | + return false |
| 79 | + } |
| 80 | + // we expire if the last access is older than the ttl |
| 81 | + return time.Since(lastAccess) > c.ttl |
| 82 | +} |
| 83 | + |
| 84 | +// ListKeys returns a list of all the keys of the secrets in the store without checking for expiration |
| 85 | +func (c *expirationCache) ListKeys() []string { |
| 86 | + c.Lock() |
| 87 | + defer c.Unlock() |
| 88 | + |
| 89 | + length := len(c.items) |
| 90 | + if length == 0 { |
| 91 | + return nil |
| 92 | + } |
| 93 | + list := make([]string, 0, length) |
| 94 | + for key := range c.items { |
| 95 | + list = append(list, key) |
| 96 | + } |
| 97 | + return list |
| 98 | +} |
| 99 | + |
| 100 | +// List returns a list of all the secrets in the store that are not expired |
| 101 | +func (c *expirationCache) List() []secret { |
| 102 | + c.Lock() |
| 103 | + defer c.Unlock() |
| 104 | + |
| 105 | + length := len(c.items) |
| 106 | + if length == 0 { |
| 107 | + return nil |
| 108 | + } |
| 109 | + list := make([]secret, 0, length) |
| 110 | + for _, entry := range c.items { |
| 111 | + if c.isExpired(entry.lastAccess) { |
| 112 | + continue |
| 113 | + } |
| 114 | + list = append(list, entry.s) |
| 115 | + } |
| 116 | + return list |
| 117 | +} |
| 118 | + |
| 119 | +// newExpirationCache creates and returns an expirationCache |
| 120 | +func newExpirationCache(ttl time.Duration) *expirationCache { |
| 121 | + return &expirationCache{ |
| 122 | + items: make(map[string]*cacheEntry), |
| 123 | + ttl: ttl, |
| 124 | + } |
| 125 | +} |
0 commit comments