Skip to content

Commit b3d079a

Browse files
committed
Adding a token getter to get service account tokens
1 parent 872b7f7 commit b3d079a

File tree

2 files changed

+266
-0
lines changed

2 files changed

+266
-0
lines changed
+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package authentication
2+
3+
import (
4+
"context"
5+
"sync"
6+
"time"
7+
8+
authv1 "k8s.io/api/authentication/v1"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/types"
11+
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
12+
"k8s.io/utils/ptr"
13+
)
14+
15+
type TokenGetter struct {
16+
client corev1.ServiceAccountsGetter
17+
expirationSeconds int64
18+
tokens map[types.NamespacedName]*authv1.TokenRequestStatus
19+
tokenLocks keyLock[types.NamespacedName]
20+
mu sync.RWMutex
21+
}
22+
23+
// Returns a token getter that can fetch tokens given a service account.
24+
// The token getter also caches tokens which helps reduce the number of requests to the API Server.
25+
// In case a cached token is expiring a fresh token is created.
26+
func NewTokenGetter(client corev1.ServiceAccountsGetter, expirationSeconds int64) *TokenGetter {
27+
return &TokenGetter{
28+
client: client,
29+
expirationSeconds: expirationSeconds,
30+
tokens: map[types.NamespacedName]*authv1.TokenRequestStatus{},
31+
tokenLocks: newKeyLock[types.NamespacedName](),
32+
}
33+
}
34+
35+
type keyLock[K comparable] struct {
36+
locks map[K]*sync.Mutex
37+
mu sync.Mutex
38+
}
39+
40+
func newKeyLock[K comparable]() keyLock[K] {
41+
return keyLock[K]{locks: map[K]*sync.Mutex{}}
42+
}
43+
44+
func (k *keyLock[K]) Lock(key K) {
45+
k.getLock(key).Lock()
46+
}
47+
48+
func (k *keyLock[K]) Unlock(key K) {
49+
k.getLock(key).Unlock()
50+
}
51+
52+
func (k *keyLock[K]) getLock(key K) *sync.Mutex {
53+
k.mu.Lock()
54+
defer k.mu.Unlock()
55+
56+
lock, ok := k.locks[key]
57+
if !ok {
58+
lock = &sync.Mutex{}
59+
k.locks[key] = lock
60+
}
61+
return lock
62+
}
63+
64+
// Returns a token from the cache if available and not expiring, otherwise creates a new token and caches it.
65+
func (t *TokenGetter) Get(ctx context.Context, key types.NamespacedName) (string, error) {
66+
t.tokenLocks.Lock(key)
67+
defer t.tokenLocks.Unlock(key)
68+
69+
t.mu.RLock()
70+
token, ok := t.tokens[key]
71+
t.mu.RUnlock()
72+
73+
expireTime := time.Time{}
74+
if ok {
75+
expireTime = token.ExpirationTimestamp.Time
76+
}
77+
78+
fiveMinutesAfterNow := metav1.Now().Add(5 * time.Minute)
79+
if expireTime.Before(fiveMinutesAfterNow) {
80+
var err error
81+
token, err = t.getToken(ctx, key)
82+
if err != nil {
83+
return "", err
84+
}
85+
t.mu.Lock()
86+
t.tokens[key] = token
87+
t.mu.Unlock()
88+
}
89+
90+
return token.Token, nil
91+
}
92+
93+
func (t *TokenGetter) getToken(ctx context.Context, key types.NamespacedName) (*authv1.TokenRequestStatus, error) {
94+
req, err := t.client.ServiceAccounts(key.Namespace).CreateToken(ctx, key.Name, &authv1.TokenRequest{Spec: authv1.TokenRequestSpec{ExpirationSeconds: ptr.To[int64](3600)}}, metav1.CreateOptions{})
95+
if err != nil {
96+
return nil, err
97+
}
98+
return &req.Status, nil
99+
}
+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package authentication
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
"time"
8+
9+
"github.com/stretchr/testify/assert"
10+
authv1 "k8s.io/api/authentication/v1"
11+
v1 "k8s.io/api/core/v1"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/runtime"
14+
"k8s.io/apimachinery/pkg/types"
15+
"k8s.io/apimachinery/pkg/watch"
16+
corev1 "k8s.io/client-go/applyconfigurations/core/v1"
17+
"k8s.io/client-go/kubernetes/fake"
18+
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
19+
ctest "k8s.io/client-go/testing"
20+
)
21+
22+
func TestNewTokenGetter(t *testing.T) {
23+
fakeClient := fake.NewSimpleClientset()
24+
fakeClient.PrependReactor("create", "serviceaccounts/token", func(action ctest.Action) (handled bool, ret runtime.Object, err error) {
25+
act, ok := action.(ctest.CreateActionImpl)
26+
if !ok {
27+
return false, nil, nil
28+
}
29+
tokenRequest := act.GetObject().(*authv1.TokenRequest)
30+
if act.Name == "test-service-account-1" {
31+
tokenRequest.Status = authv1.TokenRequestStatus{
32+
Token: "test-token-1",
33+
ExpirationTimestamp: metav1.NewTime(metav1.Now().Add(5 * time.Minute)),
34+
}
35+
}
36+
if act.Name == "test-service-account-2" {
37+
tokenRequest.Status = authv1.TokenRequestStatus{
38+
Token: "test-token-2",
39+
ExpirationTimestamp: metav1.NewTime(metav1.Now().Add(1 * time.Second)),
40+
}
41+
}
42+
43+
return true, tokenRequest, nil
44+
})
45+
tg := NewTokenGetter(fakeClient.CoreV1(), int64(5*time.Minute))
46+
t.Log("Testing NewTokenGetter with fake client")
47+
token, err := tg.Get(context.Background(), types.NamespacedName{
48+
Namespace: "test-namespace-1",
49+
Name: "test-service-account-1",
50+
})
51+
if err != nil {
52+
t.Fatalf("failed to get token: %v", err)
53+
return
54+
}
55+
t.Log("token:", token)
56+
if token != "test-token-1" {
57+
t.Errorf("token does not match")
58+
}
59+
t.Log("Testing getting token from cache")
60+
token, err = tg.Get(context.Background(), types.NamespacedName{
61+
Namespace: "test-namespace-1",
62+
Name: "test-service-account-1",
63+
})
64+
if err != nil {
65+
t.Fatalf("failed to get token from cache: %v", err)
66+
return
67+
}
68+
t.Log("token:", token)
69+
if token != "test-token-1" {
70+
t.Errorf("token does not match")
71+
}
72+
t.Log("Testing getting short lived token from fake client")
73+
token, err = tg.Get(context.Background(), types.NamespacedName{
74+
Namespace: "test-namespace-2",
75+
Name: "test-service-account-2",
76+
})
77+
if err != nil {
78+
t.Fatalf("failed to get token: %v", err)
79+
return
80+
}
81+
t.Log("token:", token)
82+
if token != "test-token-2" {
83+
t.Errorf("token does not match")
84+
}
85+
//wait for token to expire
86+
time.Sleep(1 * time.Second)
87+
t.Log("Testing getting expired token from cache")
88+
token, err = tg.Get(context.Background(), types.NamespacedName{
89+
Namespace: "test-namespace-2",
90+
Name: "test-service-account-2",
91+
})
92+
if err != nil {
93+
t.Fatalf("failed to refresh token: %v", err)
94+
return
95+
}
96+
t.Log("token:", token)
97+
if token != "test-token-2" {
98+
t.Errorf("token does not match")
99+
}
100+
}
101+
102+
type ServiceAccountsGetterImpl struct{}
103+
104+
func (ServiceAccountsGetterImpl) ServiceAccounts(namespace string) corev1client.ServiceAccountInterface {
105+
return &ServiceAccountTokenInterfaceImpl{}
106+
}
107+
108+
type ServiceAccountTokenInterfaceImpl struct{}
109+
110+
func (i ServiceAccountTokenInterfaceImpl) Apply(ctx context.Context, serviceAccount *corev1.ServiceAccountApplyConfiguration, opts metav1.ApplyOptions) (result *v1.ServiceAccount, err error) {
111+
panic("placeholder, not implemented")
112+
}
113+
114+
func (i ServiceAccountTokenInterfaceImpl) Create(ctx context.Context, serviceAccount *v1.ServiceAccount, opts metav1.CreateOptions) (*v1.ServiceAccount, error) {
115+
panic("placeholder, not implemented")
116+
}
117+
118+
func (i ServiceAccountTokenInterfaceImpl) Update(ctx context.Context, serviceAccount *v1.ServiceAccount, opts metav1.UpdateOptions) (*v1.ServiceAccount, error) {
119+
panic("placeholder, not implemented")
120+
121+
}
122+
123+
func (i ServiceAccountTokenInterfaceImpl) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
124+
panic("placeholder, not implemented")
125+
126+
}
127+
128+
func (i ServiceAccountTokenInterfaceImpl) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
129+
panic("placeholder, not implemented")
130+
131+
}
132+
133+
func (i ServiceAccountTokenInterfaceImpl) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.ServiceAccount, error) {
134+
panic("placeholder, not implemented")
135+
136+
}
137+
138+
func (i ServiceAccountTokenInterfaceImpl) List(ctx context.Context, opts metav1.ListOptions) (*v1.ServiceAccountList, error) {
139+
panic("placeholder, not implemented")
140+
141+
}
142+
143+
func (i ServiceAccountTokenInterfaceImpl) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
144+
panic("placeholder, not implemented")
145+
146+
}
147+
148+
func (i ServiceAccountTokenInterfaceImpl) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.ServiceAccount, err error) {
149+
panic("placeholder, not implemented")
150+
151+
}
152+
153+
func (ServiceAccountTokenInterfaceImpl) CreateToken(ctx context.Context, serviceAccountName string, tokenRequest *authv1.TokenRequest, opts metav1.CreateOptions) (*authv1.TokenRequest, error) {
154+
err := fmt.Errorf("error when fetching token")
155+
return nil, err
156+
}
157+
158+
func TestTokenGetter_GetToken(t *testing.T) {
159+
t.Log("Testing NewTokenGetter with test service account getter implementation")
160+
saGetter := &ServiceAccountsGetterImpl{}
161+
tg := NewTokenGetter(saGetter, int64(5*time.Minute))
162+
_, err := tg.Get(context.Background(), types.NamespacedName{
163+
Namespace: "test-namespace",
164+
Name: "test-service-account",
165+
})
166+
assert.EqualError(t, err, "error when fetching token")
167+
}

0 commit comments

Comments
 (0)