Kubernetes學習筆記之ServiceAccount TokensController源碼解析

Overview

本文章基於k8s release-1.17分支代碼,代碼位於pkg/controller/serviceaccount目錄,代碼:tokens_controller.gogit

Kubernetes學習筆記之ServiceAccount AdmissionController源碼解析 文章中,知道一個ServiceAccount對象都會引用一個 type="kubernetes.io/service-account-token" 的secret對象,這個secret對象內的 ca.crtnamespacetoken 數據會被掛載到pod內的 每個容器,供調用api-server時認證受權使用。github

當建立一個ServiceAccount對象時,引用的 type="kubernetes.io/service-account-token" 的secret對象會自動建立。好比:golang

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

問題是,這是怎麼作到的呢?shell

源碼解析

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 字段值。api

當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對象,並設置監聽器:markdown

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()來消費隊列數據,執行具體業務邏輯:app

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對象的業務邏輯,刪除邏輯也很簡單:oop

// 刪除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源碼官網解析

相關文章
相關標籤/搜索