|
|
|
@ -22,8 +22,18 @@ var ( |
|
|
|
// - Some of OIDC providers could use the refresh token rotation feature which requires calling refresh only once.
|
|
|
|
// - Some of OIDC providers could use the refresh token rotation feature which requires calling refresh only once.
|
|
|
|
// - Providers can limit the rate of requests to the token endpoint, which will lead to the error
|
|
|
|
// - Providers can limit the rate of requests to the token endpoint, which will lead to the error
|
|
|
|
// in case of many concurrent requests.
|
|
|
|
// in case of many concurrent requests.
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// The lock uses a Kubernetes annotation on the refresh token resource as a mutex.
|
|
|
|
|
|
|
|
// Only one goroutine can hold the lock at a time; others poll until the annotation
|
|
|
|
|
|
|
|
// is removed (unlocked) or expires (broken). The Kubernetes resourceVersion on put
|
|
|
|
|
|
|
|
// acts as compare-and-swap: if two goroutines race to set the annotation, only one
|
|
|
|
|
|
|
|
// succeeds and the other gets a 409 Conflict.
|
|
|
|
type refreshTokenLock struct { |
|
|
|
type refreshTokenLock struct { |
|
|
|
cli *client |
|
|
|
cli *client |
|
|
|
|
|
|
|
// waitingState tracks whether this lock instance has lost a compare-and-swap race
|
|
|
|
|
|
|
|
// and is now polling for the lock to be released. Used by Unlock to skip the
|
|
|
|
|
|
|
|
// annotation removal — only the goroutine that successfully wrote the annotation
|
|
|
|
|
|
|
|
// should remove it.
|
|
|
|
waitingState bool |
|
|
|
waitingState bool |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -31,6 +41,8 @@ func newRefreshTokenLock(cli *client) *refreshTokenLock { |
|
|
|
return &refreshTokenLock{cli: cli} |
|
|
|
return &refreshTokenLock{cli: cli} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Lock polls until the lock annotation can be set on the refresh token resource.
|
|
|
|
|
|
|
|
// Returns nil when the lock is acquired, or an error on timeout (60 attempts × 100ms).
|
|
|
|
func (l *refreshTokenLock) Lock(id string) error { |
|
|
|
func (l *refreshTokenLock) Lock(id string) error { |
|
|
|
for i := 0; i <= 60; i++ { |
|
|
|
for i := 0; i <= 60; i++ { |
|
|
|
ok, err := l.setLockAnnotation(id) |
|
|
|
ok, err := l.setLockAnnotation(id) |
|
|
|
@ -45,9 +57,12 @@ func (l *refreshTokenLock) Lock(id string) error { |
|
|
|
return fmt.Errorf("timeout waiting for refresh token %s lock", id) |
|
|
|
return fmt.Errorf("timeout waiting for refresh token %s lock", id) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Unlock removes the lock annotation from the refresh token resource.
|
|
|
|
|
|
|
|
// Only the holder of the lock (waitingState == false) performs the removal.
|
|
|
|
func (l *refreshTokenLock) Unlock(id string) { |
|
|
|
func (l *refreshTokenLock) Unlock(id string) { |
|
|
|
if l.waitingState { |
|
|
|
if l.waitingState { |
|
|
|
// Do not need to unlock for waiting goroutines, because the have not set it.
|
|
|
|
// This goroutine never successfully wrote the annotation, so there's
|
|
|
|
|
|
|
|
// nothing to remove. Another goroutine holds (or held) the lock.
|
|
|
|
return |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -64,6 +79,13 @@ func (l *refreshTokenLock) Unlock(id string) { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// setLockAnnotation attempts to acquire the lock by writing an annotation with
|
|
|
|
|
|
|
|
// an expiration timestamp. Returns (true, nil) when the caller should keep waiting,
|
|
|
|
|
|
|
|
// (false, nil) when the lock is acquired, or (false, err) on a non-retriable error.
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// The locking protocol relies on Kubernetes optimistic concurrency: every put
|
|
|
|
|
|
|
|
// includes the resource's current resourceVersion, so concurrent writes to the
|
|
|
|
|
|
|
|
// same object result in a 409 Conflict for all but one writer.
|
|
|
|
func (l *refreshTokenLock) setLockAnnotation(id string) (bool, error) { |
|
|
|
func (l *refreshTokenLock) setLockAnnotation(id string) (bool, error) { |
|
|
|
r, err := l.cli.getRefreshToken(id) |
|
|
|
r, err := l.cli.getRefreshToken(id) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
@ -77,13 +99,14 @@ func (l *refreshTokenLock) setLockAnnotation(id string) (bool, error) { |
|
|
|
|
|
|
|
|
|
|
|
val, ok := r.Annotations[lockAnnotation] |
|
|
|
val, ok := r.Annotations[lockAnnotation] |
|
|
|
if !ok { |
|
|
|
if !ok { |
|
|
|
if l.waitingState { |
|
|
|
// No annotation means the lock is free. Every goroutine — whether it's
|
|
|
|
return false, nil |
|
|
|
// a first-time caller or was previously waiting — must compete by writing
|
|
|
|
} |
|
|
|
// the annotation. The put uses the current resourceVersion, so only one
|
|
|
|
|
|
|
|
// writer succeeds; the rest get a 409 Conflict and go back to polling.
|
|
|
|
r.Annotations = lockData |
|
|
|
r.Annotations = lockData |
|
|
|
err := l.cli.put(resourceRefreshToken, r.ObjectMeta.Name, r) |
|
|
|
err := l.cli.put(resourceRefreshToken, r.ObjectMeta.Name, r) |
|
|
|
if err == nil { |
|
|
|
if err == nil { |
|
|
|
|
|
|
|
l.waitingState = false |
|
|
|
return false, nil |
|
|
|
return false, nil |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -100,24 +123,24 @@ func (l *refreshTokenLock) setLockAnnotation(id string) (bool, error) { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if !currentTime.After(until) { |
|
|
|
if !currentTime.After(until) { |
|
|
|
// waiting for the lock to be released
|
|
|
|
// Lock is held by another goroutine and has not expired yet — keep polling.
|
|
|
|
l.waitingState = true |
|
|
|
l.waitingState = true |
|
|
|
return true, nil |
|
|
|
return true, nil |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Lock time is out, lets break the lock and take the advantage
|
|
|
|
// Lock has expired (holder crashed or is too slow). Attempt to break it by
|
|
|
|
|
|
|
|
// overwriting the annotation with a new expiration. Again, only one writer
|
|
|
|
|
|
|
|
// can win the compare-and-swap race.
|
|
|
|
r.Annotations = lockData |
|
|
|
r.Annotations = lockData |
|
|
|
|
|
|
|
|
|
|
|
err = l.cli.put(resourceRefreshToken, r.ObjectMeta.Name, r) |
|
|
|
err = l.cli.put(resourceRefreshToken, r.ObjectMeta.Name, r) |
|
|
|
if err == nil { |
|
|
|
if err == nil { |
|
|
|
// break lock annotation
|
|
|
|
|
|
|
|
return false, nil |
|
|
|
return false, nil |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
l.cli.logger.Debug("break lock annotation", "error", err) |
|
|
|
l.cli.logger.Debug("break lock annotation", "error", err) |
|
|
|
if isKubernetesAPIConflictError(err) { |
|
|
|
if isKubernetesAPIConflictError(err) { |
|
|
|
l.waitingState = true |
|
|
|
l.waitingState = true |
|
|
|
// after breaking error waiting for the lock to be released
|
|
|
|
|
|
|
|
return true, nil |
|
|
|
return true, nil |
|
|
|
} |
|
|
|
} |
|
|
|
return false, err |
|
|
|
return false, err |
|
|
|
|