k8s client-go快速入门教程及源代码阅读之配置加载
从上一篇文章,我们知道使用client-go操作k8s的资源基本分为三步,1. 配置加载,2. 创建客户端,3. 使用接口,这一篇文章主要专注于使用client-go时的配置加载, 而配置加载,主要是加载认证和连接信息。
k8s支持多种认证方式,包括但不限于X509客户端证书,服务账户令牌(serviceaccount token),静态令牌文件等。比较常见的是前两者,X509客户端证书认证一般以kubeconfig
的方式存在,而服务账户令牌(serviceaccount token)一般存在于pod内部的/var/run/secrets/kubernetes.io/serviceaccount/token
X509在一定意义上可以简单的理解成SSL/TLS证书
kubeconfig
我们最常操作k8s集群的方式就是使用kubectl, 而kubectl默认会在~/.kube/config
位置找kubeconfig, 只有找到kubeconfig才能操作集群。
当然了, 也可以通过
--kubeconfig
方式手动指定,或者设置环境变量export KUBECONFIG=“指定的位置”
一个简单的kubeconfig
示例如下:
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: "base64编码的X509 CA证书"
server: "k8s集群访问地址"
name: youerning.top
contexts:
- context:
cluster: youerning.top
user: kubernetes-admin
name: [email protected]
current-context: [email protected]
kind: Config
preferences: {}
users:
- name: kubernetes-admin
user:
client-certificate-data: "base64编码的X509客户端证书"
client-key-data: "base64编码的X509客户端私钥"
代码加载过程
一般通过以下代码读取kubeconfig
package main
import (
"flag"
"fmt"
"path/filepath"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
)
func main() {
var kubeconfig *string
if home := homedir.HomeDir(); home != "" {
kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
} else {
kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
}
flag.Parse()
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
if err != nil {
panic(err)
}
// 后续可以基于这个config创建客户端
}
整个调用链是: BuildConfigFromFlags
-> NewNonInteractiveDeferredLoadingClientConfig.ClientConfig
-> DeferredLoadingClientConfig.createClientConfig
-> DirectClientConfig.ClientConfig
代码缩减如下:
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
func BuildConfigFromFlags(masterUrl, kubeconfigPath string) (*restclient.Config, error) {
// 1.
return NewNonInteractiveDeferredLoadingClientConfig(
&ClientConfigLoadingRules{ExplicitPath: kubeconfigPath},
&ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}}).ClientConfig()
}
func (config *DeferredLoadingClientConfig) ClientConfig() (*restclient.Config, error) {
// 2.
mergedClientConfig, err := config.createClientConfig()
// 3.
mergedConfig, err := mergedClientConfig.ClientConfig()
switch {
case err != nil:
case mergedConfig != nil:
if !config.loader.IsDefaultConfig(mergedConfig) {
return mergedConfig, nil
}
return mergedConfig, err
}
而上面标注的各个代码步骤是
- 通过NewNonInteractiveDeferredLoadingClientConfig封装, 一个延迟加载配置的对象
- 读取kubeconfig并转换成
clientcmd.ClientConfig
- 基于上一步生成最上层需要的
restclient.Config
对象
其中2,3可以继续深入代码。
mergedClientConfig, err := config.createClientConfig()
func (config *DeferredLoadingClientConfig) createClientConfig() (ClientConfig, error) {
// 1.
mergedConfig, err := config.loader.Load()
// 2.
config.clientConfig = NewNonInteractiveClientConfig(*mergedConfig, currentContext, config.overrides, config.loader)
return config.clientConfig, nil
}
func (rules *ClientConfigLoadingRules) Load() (*clientcmdapi.Config, error) {
errlist := []error{}
missingList := []string{}
kubeConfigFiles := []string{}
kubeConfigFiles = append(kubeConfigFiles, rules.ExplicitPath)
kubeconfigs := []*clientcmdapi.Config{}
for _, filename := range kubeConfigFiles {
// 3.
config, err := LoadFromFile(filename)
kubeconfigs = append(kubeconfigs, config)
}
// 4.
mapConfig := clientcmdapi.NewConfig()
for _, kubeconfig := range kubeconfigs {
mergo.MergeWithOverwrite(mapConfig, kubeconfig)
}
// 5.
nonMapConfig := clientcmdapi.NewConfig()
for i := len(kubeconfigs) - 1; i >= 0; i-- {
kubeconfig := kubeconfigs[i]
mergo.MergeWithOverwrite(nonMapConfig, kubeconfig)
}
// 6.
config := clientcmdapi.NewConfig()
mergo.MergeWithOverwrite(config, mapConfig)
mergo.MergeWithOverwrite(config, nonMapConfig)
return config, utilerrors.NewAggregate(errlist)
}
代码说明如下:
- 配置加载入口
- 将上一步返回的
clientcmdapi.Config
封装一下。 - 读取配置文件,先解码,再转换成
clientcmdapi.Config
- 创建一个空的config用于作为目标结果将配置文件合并进来,主要用于多个配置的情况
- k8s默认后面的配置文件覆盖前面的配置文件,所以倒序再创建一个按优先级覆盖的的config
- 先合并不含有优先级的配置文件到空config对象,再合并含优先级的配置文件到空config对象。
其实我不太懂为啥要合并两次…感觉只使用第二次就可以了.
上面的第三步其实还可以继续深入,但是会设计到编解码和转换的逻辑,这部分是k8s一个核心的逻辑,以后单独将,总的来说LoadFromFile
首先将配置文件解码成了clientcmd.api.v1.Config
对象, 继而将其转换成了clientcmd.api.Config
, 两个对象最大的不同在于切片对象转换成了map对象, 如[]NamedCluster
转成map[string]*api.Cluster
。
转换规则可参考:
tools/clientcmd/api/v1
目录下的autoConvert_v1_Config_To_api_Config
方法。
在加载配置文件之后,就可以基于clientcmdapi.Config
对象构造restclient.Config
对象了, 两者的主要不同在于前者只是包含了必要的信息,但是不够扁平化,比如前者可以配置多个集群,多个账户,但是后者只要一个就行,再就是,后者可以设置超时,代理,限流等配置,所以就需要将前者的凭证给剥离出来,然后再设置必要的默认参数,比如限流,超时等参数。
mergedConfig, err := mergedClientConfig.ClientConfig()
func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) {
// 1.
configAuthInfo, err := config.getAuthInfo()
_, err = config.getContext()
configClusterInfo, err := config.getCluster()
if err := config.ConfirmUsable(); err != nil {
return nil, err
}
// 2.
clientConfig := &restclient.Config{}
clientConfig.Host = configClusterInfo.Server
if configClusterInfo.ProxyURL != "" {
u, err := parseProxyURL(configClusterInfo.ProxyURL)
clientConfig.Proxy = http.ProxyURL(u)
}
if config.overrides != nil && len(config.overrides.Timeout) > 0 {
timeout, err := ParseTimeout(config.overrides.Timeout)
clientConfig.Timeout = timeout
}
// 3.
if u, err := url.ParseRequestURI(clientConfig.Host); err == nil && u.Opaque == "" && len(u.Path) > 1 {
u.RawQuery = ""
u.Fragment = ""
clientConfig.Host = u.String()
}
// 4.
if restclient.IsConfigTransportTLS(*clientConfig) {
var err error
var persister restclient.AuthProviderConfigPersister
userAuthPartialConfig, err := config.getUserIdentificationPartialConfig(configAuthInfo, config.fallbackReader, persister, configClusterInfo)
mergo.MergeWithOverwrite(clientConfig, userAuthPartialConfig)
serverAuthPartialConfig, err := getServerIdentificationPartialConfig(configAuthInfo, configClusterInfo)
mergo.MergeWithOverwrite(clientConfig, serverAuthPartialConfig)
}
return clientConfig, nil
}
上面的代码其实方法名已经比较明显了,大致分为四步
- 获取集群,认证信息,以及验证配置文件是否可用
- 创建一个空的
restclient.Config
对象,用于存储配置信息,如超时,代理 - 配置Host,也就是要访问的目标主机
- 配置集群,认证信息(kubeconfig一般用X509验证,可以简单的理解成TLS双向认证)
小结
k8s在加载配置文件的时候可以指定多个配置文件, 所以它的会有合并配置的操作,解码的时候首先解码成clientcmd.api.v1.Config
对象,再转成clientcmd.api.Config
(v1转__internal版本), 最后基于此对象生成restclient.Config
, 这时配置文件就有足够的信息可以连接集群了,也就能基于此创建客户端了。
可以通过环境变量KUBECONFIG指定额外的配置文件, 多个配置文件以 ; 分隔
服务账户令牌(Serviceaccount token)
单纯靠一个令牌是远远不够的,因为令牌里面并不包含连接信息,而连接信息在环境变量里面,例如
env|grep -i kube
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT=tcp://{服务地址}:443
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_HOST={服务地址}
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT_443_TCP_ADDR={服务地址}
KUBERNETES_PORT_443_TCP=tcp://{服务地址}:443
一般来说这里的服务地址是一个内部地址(clusterIP),由k8s中的service(一个叫做kubernetes的service)负责流量入口。
可以通过下面的命令验证
kubectl get service kubernetes
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP {服务地址} <none> 443/TCP 900d
其实/var/run/secrets/kubernetes.io/serviceaccount
目录下除了token文件还有ca.crt, namespace两个文件,前者是ca证书用于证书验证,后者可以知道自己在哪个namespace。
手动连接
其实有了这些信息可以手动的访问k8s集群。
当然了,除非手动给pod指定了一个比较大权限serviceaccount,否则,一般来说权限不会太大.
# 设置APIServer地址, 默认就叫这个
APISERVER=https://kubernetes.default.svc
# token所在目录
SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount
# 读取namespace
NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace)
# 读取token
TOKEN=$(cat ${SERVICEACCOUNT}/token)
# 配置CA路径, 不指定CA可以通过curl -k参数忽略证书验证
CACERT=${SERVICEACCOUNT}/ca.crt
# 访问/api
curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api
上面内容来自https://kubernetes.io/docs/tasks/run-application/access-api-from-pod/简单的翻译。
输出如下:
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "{服务地址}:6443"
}
]
}
代码加载过程
一般通过以下代码在集群内部生成restclient.Config
package main
import (
"fmt"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
func main() {
config, err := rest.InClusterConfig()
if err != nil {
panic(err.Error())
}
}
相对于前面的kubeconfig
加载,集群内部的配置文件加载简直不要太简单,就一个函数。
func InClusterConfig() (*Config, error) {
const (
tokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
rootCAFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
)
host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT")
token, err := ioutil.ReadFile(tokenFile)
tlsClientConfig := TLSClientConfig{}
if _, err := certutil.NewPool(rootCAFile); err != nil {
klog.Errorf("Expected to load root CA config from %s, but got err: %v", rootCAFile, err)
} else {
tlsClientConfig.CAFile = rootCAFile
}
return &Config{
// TODO: switch to using cluster DNS.
Host: "https://" + net.JoinHostPort(host, port),
TLSClientConfig: tlsClientConfig,
BearerToken: string(token),
BearerTokenFile: tokenFile,
}, nil
}
代码非常简单,读取环境变量找到连接的地址和端口,读取token用于配置凭证,加载CA根证书用于TLS认证,最后将这些信息填入到Config
对象并返回即可。
总结
要想连接集群必须得获取连接信息和认证信息,这部分常用的有两种方式,1.读取kubeconfig, 2.集群内部使用serviceaccount token, 前者权限比较灵活,后者一般受限比较大。当然了,你也可以在集群内部使用kubeconfig。当我们得到了restclient.Config
对象就可以创建各种客户端了,静态客户端,动态客户端。
话说大型项目好喜欢封装呀,我想这是使用设计模式的习惯吧,即尽可能不改变原有数据接口的情况下改变行为,又因为k8s是很多人花了很多时间开发的,所以很难做到一次性完美而没有冗余的设计,因此很多数据结构在不断的封装。
参考链接: https://kubernetes.io/docs/tasks/run-application/access-api-from-pod/