Skip to content
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

Merged
merged 1 commit into from
Jul 5, 2024

Conversation

skattoju
Copy link
Contributor

@skattoju skattoju commented Jul 2, 2024

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

  • API Go Documentation
  • Tests: Unit Tests (and E2E Tests, if appropriate)
  • Comprehensive Commit Messages
  • Links to related GitHub Issue(s)

@skattoju skattoju requested a review from a team as a code owner July 2, 2024 14:49
Copy link

netlify bot commented Jul 2, 2024

Deploy Preview for olmv1 ready!

Name Link
🔨 Latest commit 3b2949f
🔍 Latest deploy log https://app.netlify.com/sites/olmv1/deploys/6686f15bcfe6650009a78b01
😎 Deploy Preview https://deploy-preview-1006--olmv1.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@skattoju skattoju force-pushed the token_getter branch 2 times, most recently from 46d5ddc to b3d079a Compare July 2, 2024 15:06
@skattoju skattoju changed the title Token getter for service account tokens ✨ Adding a token getter to get service account tokens Jul 2, 2024
Copy link

codecov bot commented Jul 2, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 78.07%. Comparing base (872b7f7) to head (3b2949f).
Report is 3 commits behind head on main.

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              
Flag Coverage Δ
e2e 56.54% <ø> (ø)
unit 53.74% <100.00%> (+1.84%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

@everettraven everettraven left a 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

internal/authentication/tokengetter.go Outdated Show resolved Hide resolved
internal/authentication/tokengetter.go Outdated Show resolved Hide resolved
internal/authentication/tokengetter.go Outdated Show resolved Hide resolved
internal/authentication/tokengetter.go Outdated Show resolved Hide resolved
internal/authentication/tokengetter_test.go Outdated Show resolved Hide resolved
Comment on lines 66 to 53
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
}
Copy link
Member

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.

Copy link
Contributor

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

Copy link
Contributor Author

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))
Copy link
Contributor

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?

Copy link
Member

@joelanford joelanford Jul 3, 2024

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)

Copy link
Contributor Author

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..

Copy link
Contributor Author

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)
}

Copy link
Member

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
Copy link
Member

@joelanford joelanford Jul 3, 2024

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.

Copy link
Contributor Author

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 👍

Comment on lines 94 to 111
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
}
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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

@skattoju skattoju force-pushed the token_getter branch 4 times, most recently from 0f3c60d to d3c041f Compare July 4, 2024 16:57
Copy link
Contributor

@everettraven everettraven left a 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!

@everettraven everettraven added this pull request to the merge queue Jul 5, 2024
Merged via the queue into operator-framework:main with commit 5c83e91 Jul 5, 2024
17 checks passed
Comment on lines +107 to +109
if metav1.Now().Sub(token.ExpirationTimestamp.Time) > expiredDuration {
delete(t.tokens, key)
}
Copy link
Member

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.

Copy link
Contributor Author

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))
Copy link
Member

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:

Suggested change
rotationThresholdAfterNow := metav1.Now().Add(t.expirationDuration * (RotationThresholdPercentage / 100))
rotationThresholdAfterNow := metav1.Now().Add(time.Duration(float64(t.expirationDuration) * RotationThresholdPercentage)

Copy link
Member

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))},
Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Spec: authenticationv1.TokenRequestSpec{ExpirationSeconds: ptr.To[int64](int64(t.expirationDuration))},
Spec: authenticationv1.TokenRequestSpec{ExpirationSeconds: ptr.To[int64](int64(t.expirationDuration/time.Second))},

perdasilva pushed a commit to LalatenduMohanty/operator-controller that referenced this pull request Aug 13, 2024
@skattoju skattoju mentioned this pull request Sep 25, 2024
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement a Go struct for fetching and caching authentication tokens for a ServiceAccount
3 participants