本文章基於 k8s release-1.17 分支代碼,代碼位於pkg/controller/serviceaccount目錄,代碼:tokens_controller.go 。
在 Kubernetes 學習筆記之 ServiceAccount AdmissionController 源碼解析 文章中,知道一個 ServiceAccount 對象都會引用一個type="kubernetes.io/service-account-token" 的 secret 對象,這個 secret 對象內的 ca.crt 、 namespace 和 token 數據會被掛載到 pod 內的每一個容器,供調用 api-server 時認證授權使用。
當創建一個 ServiceAccount 對象時,引用的 type="kubernetes.io/service-account-token" 的 secret 對象會自動創建。比如:
kubectl create sa test-sa1 -o yaml
kubectl get sa test-sa1 -o yaml
kubectl get secret test-sa1-token-jg6lm -o yaml
serviceaccount_token問題是,這是怎麼做到的呢?
源碼解析TokensController 實例化實際上這是由 kube-controller-manager 的 TokenController 實現的,kube-controller-manager 進程的啟動參數有 --root-ca-file 和 --service-account-private-key-file ,其中, --root-ca-file 就是上圖中的 ca.crt 數據, --service-account-private-key-file 是用來籤名上圖中的 jwt token 數據,即 token 欄位值。
當 kube-controller-manager 進程在啟動時,會首先實例化 TokensController,並傳遞實例化所需相關參數。其中,從啟動參數中讀取 ca 根證書和私鑰文件內容,並且使用 serviceaccount.JWTTokenGenerator() 函數生成 jwt token,代碼在 L546-L592 :
func (c serviceAccountTokenControllerStarter) startServiceAccountTokenController(ctx ControllerContext) (http.Handler, bool, error) {
// ...
// 讀取--service-account-private-key-file私鑰文件
privateKey, err := keyutil.PrivateKeyFromFile(ctx.ComponentConfig.SAController.ServiceAccountKeyFile)
if err != nil {
return nil, true, fmt.Errorf("error reading key for service account token controller: %v", err)
}
// 讀取--root-ca-file的值作為ca,沒有傳則使用kubeconfig文件內的ca值
var rootCA []byte
if ctx.ComponentConfig.SAController.RootCAFile != "" {
if rootCA, err = readCA(ctx.ComponentConfig.SAController.RootCAFile); err != nil {
return nil, true, fmt.Errorf("error parsing root-ca-file at %s: %v", ctx.ComponentConfig.SAController.RootCAFile, err)
}
} else {
rootCA = c.rootClientBuilder.ConfigOrDie("tokens-controller").CAData
}
// 使用tokenGenerator來生成jwt token,並且使用--service-account-private-key-file私鑰來籤名jwt token
tokenGenerator, err := serviceaccount.JWTTokenGenerator(serviceaccount.LegacyIssuer, privateKey)
//...
// 實例化TokensController
controller, err := serviceaccountcontroller.NewTokensController(
ctx.InformerFactory.Core().V1().ServiceAccounts(), // ServiceAccount informer
ctx.InformerFactory.Core().V1().Secrets(), // Secret informer
c.rootClientBuilder.ClientOrDie("tokens-controller"),
serviceaccountcontroller.TokensControllerOptions{
TokenGenerator: tokenGenerator,
RootCA: rootCA,
},
)
// ...
// 消費隊列數據
go controller.Run(int(ctx.ComponentConfig.SAController.ConcurrentSATokenSyncs), ctx.Stop)
// 啟動ServiceAccount informer和Secret informer
ctx.InformerFactory.Start(ctx.Stop)
return nil, true, nil
}TokensController 實例化時,會去監聽 ServiceAccount 和 kubernetes.io/service-account-token 類型的 Secret 對象,並設置監聽器:
func NewTokensController(serviceAccounts informers.ServiceAccountInformer, secrets informers.SecretInformer, cl clientset.Interface, options TokensControllerOptions) (*TokensController, error) {
e := &TokensController{
// ...
// 分別為service和secret創建對應的限速隊列queue,用來存儲事件數據
syncServiceAccountQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "serviceaccount_tokens_service"),
syncSecretQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "serviceaccount_tokens_secret"),
}
// ...
e.serviceAccounts = serviceAccounts.Lister()
e.serviceAccountSynced = serviceAccounts.Informer().HasSynced
// 註冊service account資源對象的事件監聽,把事件放入syncServiceAccountQueue限速隊列中
serviceAccounts.Informer().AddEventHandlerWithResyncPeriod(
cache.ResourceEventHandlerFuncs{
AddFunc: e.queueServiceAccountSync,
UpdateFunc: e.queueServiceAccountUpdateSync,
DeleteFunc: e.queueServiceAccountSync,
},
options.ServiceAccountResync,
)
// ...
secrets.Informer().AddEventHandlerWithResyncPeriod(
cache.FilteringResourceEventHandler{
FilterFunc: func(obj interface{}) bool {
switch t := obj.(type) {
case *v1.Secret:
return t.Type == v1.SecretTypeServiceAccountToken // 這裡過濾出"kubernetes.io/service-account-token"類型的secret
default:
utilruntime.HandleError(fmt.Errorf("object passed to %T that is not expected: %T", e, obj))
return false
}
},
// 同理,註冊secret資源對象的事件監聽,把事件放入syncSecretQueue限速隊列中
Handler: cache.ResourceEventHandlerFuncs{
AddFunc: e.queueSecretSync,
UpdateFunc: e.queueSecretUpdateSync,
DeleteFunc: e.queueSecretSync,
},
},
options.SecretResync,
)
return e, nil
}
// 把service對象存進syncServiceAccountQueue
func (e *TokensController) queueServiceAccountSync(obj interface{}) {
if serviceAccount, ok := obj.(*v1.ServiceAccount); ok {
e.syncServiceAccountQueue.Add(makeServiceAccountKey(serviceAccount))
}
}
// 把secret對象存進syncSecretQueue
func (e *TokensController) queueSecretSync(obj interface{}) {
if secret, ok := obj.(*v1.Secret); ok {
e.syncSecretQueue.Add(makeSecretQueueKey(secret))
}
}把數據存入隊列後,goroutine 調用 controller.Run()來消費隊列數據,執行具體業務邏輯:
func (e *TokensController) Run(workers int, stopCh <-chan struct{}) {
// ...
for i := 0; i < workers; i++ {
go wait.Until(e.syncServiceAccount, 0, stopCh)
go wait.Until(e.syncSecret, 0, stopCh)
}
<-stopCh
// ...
}
Controller 業務邏輯ServiceAccount 的增刪改查當用戶增刪改查 ServiceAccount 時,需要判斷兩個業務邏輯:當刪除 ServiceAccount 時,需要刪除其引用的 Secret 對象;當添加/更新 ServiceAccount 時,需要確保引用的 Secret 對象存在,如果不存在,則創建個新 Secret 對象。可見代碼:
func (e *TokensController) syncServiceAccount() {
// ...
// 從本地緩存中查詢service account對象
sa, err := e.getServiceAccount(saInfo.namespace, saInfo.name, saInfo.uid, false)
switch {
case err != nil:
klog.Error(err)
retry = true
case sa == nil:
// 該service account已經被刪除,需要刪除其引用的secret對象
sa = &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Namespace: saInfo.namespace, Name: saInfo.name, UID: saInfo.uid}}
retry, err = e.deleteTokens(sa)
default:
// 創建/更新service account時,需要確保其引用的secret對象存在,不存在則新建一個secret對象
retry, err = e.ensureReferencedToken(sa)
// ...
}
}先看如何刪除其引用的 secret 對象的業務邏輯,刪除邏輯也很簡單:
// 刪除service account引用的secret對象
func (e *TokensController) deleteTokens(serviceAccount *v1.ServiceAccount) ( /*retry*/ bool, error) {
// list出該service account所引用的所有secret
tokens, err := e.listTokenSecrets(serviceAccount)
// ...
for _, token := range tokens {
// 再一個個刪除secret對象
r, err := e.deleteToken(token.Namespace, token.Name, token.UID)
// ...
}
// ...
}
func (e *TokensController) deleteToken(ns, name string, uid types.UID) ( /*retry*/ bool, error) {
// ...
// 對api-server發起刪除secret對象資源的請求
err := e.client.CoreV1().Secrets(ns).Delete(name, opts)
// ...
}這裡關鍵是如何找到 serviceAccount 所引用的所有 secret 對象,不能通過 serviceAccount.secrets 欄位來查找,因為這個欄位值只是所有 secrets 的部分值。實際上,從緩存中,首先 list 出該 serviceAccount 對象所在的 namespace 下所有 secrets,然後過濾出 type=kubernetes.io/service-account-token 類型的secret,然後查找 secret annotation 中的 kubernetes.io/service-account.name 應該是 serviceAccount.Name 值,和 kubernetes.io/service-account.uid應該是 serviceAccount.UID 值。只有滿足以上條件,才是該 serviceAccount 所引用的 secrets。首先從緩存中找出該 namespace 下所有 secrets,這裡需要注意的是緩存對象 updatedSecrets 使用的是 LRU(Least Recently Used) Cache 最少使用緩存,減少內存使用:
func (e *TokensController) listTokenSecrets(serviceAccount *v1.ServiceAccount) ([]*v1.Secret, error) {
// 從LRU cache中查找出該namespace下所有secrets
namespaceSecrets, err := e.updatedSecrets.ByIndex("namespace", serviceAccount.Namespace)
// ...
items := []*v1.Secret{}
for _, obj := range namespaceSecrets {
secret := obj.(*v1.Secret)
// 判斷只有符合相應條件才是該serviceAccount所引用的secret
if serviceaccount.IsServiceAccountToken(secret, serviceAccount) {
items = append(items, secret)
}
}
return items, nil
}
// 判斷條件
func IsServiceAccountToken(secret *v1.Secret, sa *v1.ServiceAccount) bool {
if secret.Type != v1.SecretTypeServiceAccountToken {
return false
}
name := secret.Annotations[v1.ServiceAccountNameKey]
uid := secret.Annotations[v1.ServiceAccountUIDKey]
if name != sa.Name {
return false
}
if len(uid) > 0 && uid != string(sa.UID) {
return false
}
return true
}所以,當 ServiceAccount 對象刪除時,需要刪除其所引用的所有 Secrets 對象。
再看如何新建 secret 對象的業務邏輯。當新建或更新 ServiceAccount 對象時,需要確保其引用的 Secrets 對象存在,不存在就需要新建個 secret 對象:
// 檢查該ServiceAccount對象引用的secrets對象存在,不存在則新建
func (e *TokensController) ensureReferencedToken(serviceAccount *v1.ServiceAccount) (bool, error) {
// 首先確保serviceAccount.secrets欄位值中的secret都存在
if hasToken, err := e.hasReferencedToken(serviceAccount); err != nil {
return false, err
} else if hasToken {
return false, nil
}
// 對api-server發起請求查找該serviceAccount對象
serviceAccounts := e.client.CoreV1().ServiceAccounts(serviceAccount.Namespace)
liveServiceAccount, err := serviceAccounts.Get(serviceAccount.Name, metav1.GetOptions{})
// ...
if liveServiceAccount.ResourceVersion != serviceAccount.ResourceVersion {
return true, nil
}
// 如果是新建的ServiceAccount,則給ServiceAccount.secrets欄位值添加個默認生成的secret對象
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secret.Strategy.GenerateName(fmt.Sprintf("%s-token-", serviceAccount.Name)),
Namespace: serviceAccount.Namespace,
Annotations: map[string]string{
v1.ServiceAccountNameKey: serviceAccount.Name, // 這裡使用serviceAccount.Name來作為annotation
v1.ServiceAccountUIDKey: string(serviceAccount.UID), // 這裡使用serviceAccount.UID來作為annotation
},
},
Type: v1.SecretTypeServiceAccountToken,
Data: map[string][]byte{},
}
// 生成jwt token,該token是用私鑰籤名的
token, err := e.token.GenerateToken(serviceaccount.LegacyClaims(*serviceAccount, *secret))
// ...
secret.Data[v1.ServiceAccountTokenKey] = []byte(token)
secret.Data[v1.ServiceAccountNamespaceKey] = []byte(serviceAccount.Namespace)
if e.rootCA != nil && len(e.rootCA) > 0 {
secret.Data[v1.ServiceAccountRootCAKey] = e.rootCA
}
// 向api-server中創建該secret對象
createdToken, err := e.client.CoreV1().Secrets(serviceAccount.Namespace).Create(secret)
// ...
// 寫入LRU cache中
e.updatedSecrets.Mutation(createdToken)
err = clientretry.RetryOnConflict(clientretry.DefaultRetry, func() error {
// ...
// 把新建的secrets對象放入ServiceAccount.Secrets欄位中,然後更新ServiceAccount對象
liveServiceAccount.Secrets = append(liveServiceAccount.Secrets, v1.ObjectReference{Name: secret.Name})
if _, err := serviceAccounts.Update(liveServiceAccount); err != nil {
return err
}
// ...
})
// ...
}所以,當 ServiceAccount 對象新建時,需要新建個新的 Secret 對象作為 ServiceAccount 對象的引用。業務代碼還是比較簡單的。
Secret 的增刪改查當增刪改查 secret 時,刪除 secret 時同時需要刪除 serviceAccount 對象下的 secrets 欄位引用;
func (e *TokensController) syncSecret() {
// ...
// 從LRU Cache中查找該secret
secret, err := e.getSecret(secretInfo.namespace, secretInfo.name, secretInfo.uid, false)
switch {
case err != nil:
klog.Error(err)
retry = true
case secret == nil:
// 刪除secret時:
// 查找serviceAccount對象是否存在
if sa, saErr := e.getServiceAccount(secretInfo.namespace, secretInfo.saName, secretInfo.saUID, false); saErr == nil && sa != nil {
// 從service中刪除其secret引用
if err := clientretry.RetryOnConflict(RemoveTokenBackoff, func() error {
return e.removeSecretReference(secretInfo.namespace, secretInfo.saName, secretInfo.saUID, secretInfo.name)
}); err != nil {
klog.Error(err)
}
}
default:
// 新建或更新secret時:
// 查找serviceAccount對象是否存在
sa, saErr := e.getServiceAccount(secretInfo.namespace, secretInfo.saName, secretInfo.saUID, true)
switch {
case saErr != nil:
klog.Error(saErr)
retry = true
case sa == nil:
// 如果serviceAccount都已經不存在,刪除secret
if retriable, err := e.deleteToken(secretInfo.namespace, secretInfo.name, secretInfo.uid); err != nil {
// ...
}
default:
// 新建或更新secret時,且serviceAccount存在時,查看是否需要更新secret中的ca/namespace/token欄位值
// 當然,新建secret時,肯定需要更新
if retriable, err := e.generateTokenIfNeeded(sa, secret); err != nil {
// ...
}
}
}
}所以,對 kubernetes.io/service-account-token 類型的 secret 增刪改查的業務邏輯,也比較簡單。重點是學習下官方 golang 代碼編寫和一些有關 k8s api的使用,對自己二次開發 k8s 大有裨益。
總結本文主要學習 TokensController 是如何監聽 ServiceAccount 對象和 kubernetes.io/service-account-token 類型 Secret 對象的增刪改查,並做了相應的業務邏輯處理,比如新建 ServiceAccount 時需要新建對應的 Secret 對象,刪除 ServiceAccount 需要刪除對應的 Secret 對象,以及新建 Secret 對象時,還需要給該 Secret 對象補上 ca.crt/namespace/token欄位值,以及一些邊界條件的處理邏輯等等。
同時,官方的 TokensController 代碼編寫規範,以及對 k8s api 的應用,邊界條件的處理,以及使用了 LRU Cache 緩存等等,都值得在自己的項目裡參考。
學習要點tokens_controller.go L106 使用了LRU cache。
參考文獻為 Pod 配置服務帳戶
服務帳號令牌 Secret
serviceaccounts-controller 源碼官網解析