-
Notifications
You must be signed in to change notification settings - Fork 54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
✨ Adding a token getter to get service account tokens #1006
Conversation
✅ Deploy Preview for olmv1 ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
46d5ddc
to
b3d079a
Compare
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #1006 +/- ##
==========================================
+ Coverage 77.19% 78.07% +0.87%
==========================================
Files 17 18 +1
Lines 1206 1254 +48
==========================================
+ Hits 931 979 +48
Misses 193 193
Partials 82 82
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall I think this looks really good, great work! There are a few smaller changes I'd recommend
func (k *keyLock[K]) getLock(key K) *sync.Mutex { | ||
k.mu.Lock() | ||
defer k.mu.Unlock() | ||
|
||
lock, ok := k.locks[key] | ||
if !ok { | ||
lock = &sync.Mutex{} | ||
k.locks[key] = lock | ||
} | ||
return lock | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The number of locks will grow unbounded here, right? Maybe this is unnecessary complexity to start off with. WDYT about skipping the keyLock altogether. For now, maybe it's fine to get a global lock in the Get
method. That would allow only one token to be requested at a time. If that becomes a problem, we can introduce key locks later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking back through this a second time I was wondering the same thing. I get why the key lock was created but agree that this is probably more complex than necessary for a first pass
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok ill remove the key locks for now
expireTime = token.ExpirationTimestamp.Time | ||
} | ||
|
||
expirationSecondsAfterNow := metav1.Now().Add(time.Duration(t.expirationSeconds)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the expiration seconds field here feels unintuitive to me. Maybe we should add another field to the struct called something like rotationThreshold
that is a time.Duration
type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or maybe we make it some hardcoded fraction of expirationSeconds
? Maybe if we're within 10% or something (i.e. if expiration is 100s, then we would get a new token anytime after 90s)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah i guess setting that to expirationSeconds may be too much.. I will add a rotationThreshold so its configurable..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So if expirationSeconds is 100s and we set rotationThreshold to 10s we we would create a new token if its been more than 90s since the token was created like you suggested 👍
for _, opt := range options { | ||
opt(tokenGetter) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we allow expiration seconds and rotation threshold to be independently configurable, we probably need to do some sanity validation to make sure they make sense.
But I'm not sure we actually need rotation threshold to be configurable if we just make it expirationDuration * 0.1
or whatever we think is a reasonable ratio. I feel like we may be over-engineering this.
func NewTokenGetter(client corev1.ServiceAccountsGetter, options ...TokenGetterOption) *TokenGetter { | ||
tokenGetter := &TokenGetter{ | ||
client: client, | ||
expirationSeconds: int64(5 * time.Minute), // default token ttl |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should change this to a time.Duration
type and change the name to expirationDuration
. The name ("Seconds" suffix) and value (integer number of nanoseconds in 5 minutes) lead a reader to an incorrect conclusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess that was int64 because the TokenRequest api takes an pointer to an int64.. its better as a time.Duration 👍
func (t *TokenGetter) Clean(ctx context.Context, key types.NamespacedName) { | ||
t.mu.RLock() | ||
defer t.mu.RUnlock() | ||
delete(t.tokens, key) | ||
} | ||
|
||
func (t *TokenGetter) TokenExists(ctx context.Context, key types.NamespacedName) bool { | ||
t.mu.RLock() | ||
defer t.mu.RUnlock() | ||
_, ok := t.tokens[key] | ||
return ok | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do we envision Clean
and TokenExists
being used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clean would be to remove entries from the cache when extensions are uninstalled or unused items need to be cleaned up for some other reason. TokenExists is debatable, as I added to make it testable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are made redundant by reapExpiredTokens
so I have removed them
0f3c60d
to
d3c041f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for bearing with us on all the reviews @skattoju - I think this is in a good state now!
if metav1.Now().Sub(token.ExpirationTimestamp.Time) > expiredDuration { | ||
delete(t.tokens, key) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would we keep expired tokens around? Seems like they could be reaped as soon as they expire.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that you mention it, it seems like we can get rid of that expiredDuration parameter 🤔
} | ||
|
||
// Create a new token if the cached token expires within DurationPercentage of expirationDuration from now | ||
rotationThresholdAfterNow := metav1.Now().Add(t.expirationDuration * (RotationThresholdPercentage / 100)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will rotationThresholdAfterNow
always evaluate to 0
when RotationThresholdPercentage < 100
? Seems like (RotationThresholdPercentage / 100)
is an integer division.
RotationThresholdPercentage should probably be a float64
so that we can do something like this:
rotationThresholdAfterNow := metav1.Now().Add(t.expirationDuration * (RotationThresholdPercentage / 100)) | |
rotationThresholdAfterNow := metav1.Now().Add(time.Duration(float64(t.expirationDuration) * RotationThresholdPercentage) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, any reason for RotationThresholdPercentage
to be exported? Seems like we could unexport it.
req, err := t.client.ServiceAccounts(key.Namespace).CreateToken(ctx, | ||
key.Name, | ||
&authenticationv1.TokenRequest{ | ||
Spec: authenticationv1.TokenRequestSpec{ExpirationSeconds: ptr.To[int64](int64(t.expirationDuration))}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
t.expirationDuration
needs to be converted to an integer number of seconds to be used in the TokenRequest API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spec: authenticationv1.TokenRequestSpec{ExpirationSeconds: ptr.To[int64](int64(t.expirationDuration))}, | |
Spec: authenticationv1.TokenRequestSpec{ExpirationSeconds: ptr.To[int64](int64(t.expirationDuration/time.Second))}, |
Description
As part of #737 user provided service accounts will be used to manage cluster extensions, the token getter implemented in this PR enables this by providing an interface to fetch tokens given a service account. It also implements a cache where fetched tokens are stored saving requests to the api server for subsequent requests. Fixes #972
Reviewer Checklist