147 lines
4.4 KiB
Go
147 lines
4.4 KiB
Go
// Copyright 2023 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package tokenclient
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
authenticationv1 "k8s.io/api/authentication/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/client-go/kubernetes"
|
|
"k8s.io/utils/clock"
|
|
|
|
"go.pinniped.dev/internal/backoff"
|
|
"go.pinniped.dev/internal/plog"
|
|
)
|
|
|
|
type WhatToDoWithTokenFunc func(authenticationv1.TokenRequestStatus, metav1.Duration) error
|
|
|
|
type TokenClient struct {
|
|
namespace string
|
|
serviceAccountName string
|
|
k8sClient kubernetes.Interface
|
|
whatToDoWithToken WhatToDoWithTokenFunc
|
|
expirationSeconds int64
|
|
clock clock.Clock
|
|
logger plog.Logger
|
|
}
|
|
|
|
type Opt func(client *TokenClient)
|
|
|
|
func WithExpirationSeconds(expirationSeconds int64) Opt {
|
|
return func(client *TokenClient) {
|
|
client.expirationSeconds = expirationSeconds
|
|
}
|
|
}
|
|
|
|
func New(
|
|
namespace string,
|
|
serviceAccountName string,
|
|
k8sClient kubernetes.Interface,
|
|
whatToDoWithToken WhatToDoWithTokenFunc,
|
|
logger plog.Logger,
|
|
opts ...Opt,
|
|
) *TokenClient {
|
|
client := &TokenClient{
|
|
namespace: namespace,
|
|
serviceAccountName: serviceAccountName,
|
|
k8sClient: k8sClient,
|
|
whatToDoWithToken: whatToDoWithToken,
|
|
expirationSeconds: 600,
|
|
clock: clock.RealClock{},
|
|
logger: logger,
|
|
}
|
|
for _, opt := range opts {
|
|
opt(client)
|
|
}
|
|
return client
|
|
}
|
|
|
|
type howToFetchTokenFromAPIServer func(tokenRequest *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error)
|
|
|
|
func (tokenClient TokenClient) Start(ctx context.Context) {
|
|
sleeper := make(chan time.Time, 1)
|
|
// Make sure that the <-sleeper below gets run once immediately.
|
|
sleeper <- time.Now()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
tokenClient.logger.Info("TokenClient was cancelled and is stopping")
|
|
return
|
|
case <-sleeper:
|
|
var tokenTTL metav1.Duration
|
|
err := backoff.WithContext(ctx, &backoff.InfiniteBackoff{
|
|
Duration: 10 * time.Millisecond,
|
|
MaxDuration: 5 * time.Second,
|
|
Factor: 2.0,
|
|
}, func(ctx context.Context) (bool, error) {
|
|
var err error
|
|
var tokenRequestStatus authenticationv1.TokenRequestStatus
|
|
tokenRequestStatus, tokenTTL, err = tokenClient.fetchToken(
|
|
func(tokenRequest *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) {
|
|
return tokenClient.k8sClient.CoreV1().ServiceAccounts(tokenClient.namespace).CreateToken(
|
|
ctx,
|
|
tokenClient.serviceAccountName,
|
|
tokenRequest,
|
|
metav1.CreateOptions{})
|
|
},
|
|
)
|
|
|
|
if err != nil {
|
|
tokenClient.logger.Warning(fmt.Sprintf("Could not fetch token: %s\n", err))
|
|
// We got an error. Swallow it and ask for retry.
|
|
return false, nil
|
|
}
|
|
|
|
err = tokenClient.whatToDoWithToken(tokenRequestStatus, tokenTTL)
|
|
if err != nil {
|
|
tokenClient.logger.Warning(fmt.Sprintf("unable to pass token to the caller: %s\n", err))
|
|
// We got an error. Swallow it and ask for retry.
|
|
return false, nil
|
|
}
|
|
// We got a token. Stop backing off.
|
|
return true, nil
|
|
})
|
|
if err != nil {
|
|
// We were cancelled during our WithContext. We know it was not due to some other
|
|
// error because our last argument to WithContext above never returns any errors.
|
|
return
|
|
}
|
|
// Schedule ourselves to wake up in the future.
|
|
time.AfterFunc(tokenTTL.Duration*4/5, func() {
|
|
sleeper <- time.Now()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func (tokenClient TokenClient) fetchToken(
|
|
howToFetchTokenFromAPIServer howToFetchTokenFromAPIServer,
|
|
) (authenticationv1.TokenRequestStatus, metav1.Duration, error) {
|
|
tokenClient.logger.Debug(fmt.Sprintf("refreshing cache at time=%s\n", tokenClient.clock.Now().Format(time.RFC3339)))
|
|
|
|
tokenRequestInput := &authenticationv1.TokenRequest{
|
|
Spec: authenticationv1.TokenRequestSpec{
|
|
ExpirationSeconds: &tokenClient.expirationSeconds,
|
|
},
|
|
}
|
|
|
|
tokenRequest, err := howToFetchTokenFromAPIServer(tokenRequestInput)
|
|
|
|
emptyMetav1Duration := metav1.Duration{Duration: 0}
|
|
if err != nil {
|
|
return authenticationv1.TokenRequestStatus{}, emptyMetav1Duration, errors.Wrap(err, "error creating token")
|
|
}
|
|
|
|
if tokenRequest == nil {
|
|
return authenticationv1.TokenRequestStatus{}, emptyMetav1Duration, errors.New("tokenRequest is nil after request")
|
|
}
|
|
|
|
ttl := metav1.Duration{Duration: tokenRequest.Status.ExpirationTimestamp.Sub(tokenClient.clock.Now())}
|
|
return tokenRequest.Status, ttl, nil
|
|
}
|