乐趣区

关于kubernetes:Kubernetes学习笔记之ServiceAccount-TokensController源码解析

Overview

本文章基于 k8s release-1.17 分支代码,代码位于 pkg/controller/serviceaccount 目录,代码:tokens_controller.go

Kubernetes 学习笔记之 ServiceAccount AdmissionController 源码解析 文章中,晓得一个 ServiceAccount 对象都会援用一个
type="kubernetes.io/service-account-token" 的 secret 对象,这个 secret 对象内的 ca.crtnamespacetoken 数据会被挂载到 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

问题是,这是怎么做到的呢?

源码解析

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 源码官网解析

退出移动版