学习源码最头疼的是找到入口开始学习,刚开始学习client-go源码的时候,感觉无从下手,后面参考了很多博客,慢慢摸索,从连接k8s集群开始,逐步展开,就可以轻松读懂client-go源码。当然,在学习源码的过程中,也要学习源码的编码风格,学习源码的编程思路,对自己的编程能力也有很大的提高,开始第一篇文章,找到client-go的入口,连接k8s集群。
go get k8s.io/client-go
管理k8s的第一步先要连接集群,因此我们先找到client-go源码的入口。 在Github官网上,我们可以找到官方给的example程序。
点击后可以看到下面的界面,里面有一个main.go,这就是我们要找到的入口了。
点击main.go就可以看到一个示例程序。
package main
import (
"context"
"flag"
"fmt"
"os"
"path/filepath"
"time"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
func main() {
var kubeconfig *string
if home := 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()
// use the current context in kubeconfig
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
if err != nil {
panic(err.Error())
}
// create the clientset
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err.Error())
}
for {
pods, err := clientset.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
if err != nil {
panic(err.Error())
}
fmt.Printf("There are %d pods in the cluster\n", len(pods.Items))
// Examples for error handling:
// - Use helper functions like e.g. errors.IsNotFound()
// - And/or cast to StatusError and use its properties like e.g. ErrStatus.Message
namespace := "default"
pod := "example-xxxxx"
_, err = clientset.CoreV1().Pods(namespace).Get(context.TODO(), pod, metav1.GetOptions{})
if errors.IsNotFound(err) {
fmt.Printf("Pod %s in namespace %s not found\n", pod, namespace)
} else if statusError, isStatus := err.(*errors.StatusError); isStatus {
fmt.Printf("Error getting pod %s in namespace %s: %v\n",
pod, namespace, statusError.ErrStatus.Message)
} else if err != nil {
panic(err.Error())
} else {
fmt.Printf("Found pod %s in namespace %s\n", pod, namespace)
}
time.Sleep(10 * time.Second)
}
}
func homeDir() string {
if h := os.Getenv("HOME"); h != "" {
return h
}
return os.Getenv("USERPROFILE") // windows
}
在示例代码中有两个函数我们先从main函数开始分析。
(1) main函数中先定义了一个字符串指针变量kubeconfig,接着是调用homeDir函数获取当前家目录,并判断家目录是否为空,如果为空则kubeconfig也为空,不为空则kubeconfig为k8s配置文件目录。从这里看出,client-go应该是通过config文件连接k8s集群的。
(2)接着调用了BuildConfigFromFlags("", *kubeconfig)函数。我们看一下BuildConfigFromFlags函数做了什么。
// BuildConfigFromFlags is a helper function that builds configs from a master
// url or a kubeconfig filepath. These are passed in as command line flags for cluster
// components. Warnings should reflect this usage. If neither masterUrl or kubeconfigPath
// are passed in we fallback to inClusterConfig. If inClusterConfig fails, we fallback
// to the default config.
func BuildConfigFromFlags(masterUrl, kubeconfigPath string) (*restclient.Config, error) {
if kubeconfigPath == "" && masterUrl == "" {
klog.Warningf("Neither --kubeconfig nor --master was specified. Using the inClusterConfig. This might not work.")
kubeconfig, err := restclient.InClusterConfig()
if err == nil {
return kubeconfig, nil
}
klog.Warning("error creating inClusterConfig, falling back to default config: ", err)
}
return NewNonInteractiveDeferredLoadingClientConfig(
&ClientConfigLoadingRules{ExplicitPath: kubeconfigPath},
&ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}}).ClientConfig()
}
从函数的注释可以看出他的主要功能是根据config文件或者masterUrl构建一个Config结构体,该结构体包含了,集群的主要配置信息,原来除了通过配置文件的方式连接k8s集群外,还可以通过masterurl连接集群,官方比较推荐的是通过config文件连接集群。感兴趣的可以看一下restclient.Config,这里不做赘述。
(3)接着调用kubernetes.NewForConfig(config),通过config结构体与k8s集群建立连接。
(4)最后是一个每十秒采集一次pod信息的for循环,在例子中有两个需要注意的点。
第一个是获取集群中的所有pod:
pods, err := clientset.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
if err != nil {
panic(err.Error())
}
fmt.Printf("There are %d pods in the cluster\n", len(pods.Items))
从上面的第一行代码就可以看出,这行代码的意思是列出集群中的pod,List方法有两个参数,第一个是context上下文,这个可以使用context.TODO生成一个空的上下文,第二个参数,相当于k8s中的Selector,在metav1.ListOptions中你可以进行定义。另外Pods方法中有一个参数是用来筛选NameSpace的,这个Pods方法调用的是newPods:
// newPods returns a Pods
func newPods(c *CoreV1Client, namespace string) *pods {
return &pods{
client: c.RESTClient(),
ns: namespace,
}
}
可以看到nowPods返回的是一个pods类型的指针,pods中有两个参数一个是命名空间,一个是连接k8s集群的指针client。
接下来我们看List方法的源码:
// List takes label and field selectors, and returns the list of Pods that match those selectors.
func (c *pods) List(ctx context.Context, opts metav1.ListOptions) (result *v1.PodList, err error) {
var timeout time.Duration
if opts.TimeoutSeconds != nil {
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
}
result = &v1.PodList{}
err = c.client.Get().
Namespace(c.ns).
Resource("pods").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Do(ctx).
Into(result)
return
}
可以看到List调用了六个方法,我们来逐一分析:
// Get begins a GET request. Short for c.Verb("GET").
func (c *RESTClient) Get() *Request {
return c.Verb("GET")
}
Get方法调用Verb方法返回一个Request对象,我们看一下Verb方法:
func (c *RESTClient) Verb(verb string) *Request {
return NewRequest(c).Verb(verb)
}
// NewRequest creates a new request helper object for accessing runtime.Objects on a server.
func NewRequest(c *RESTClient) *Request {
var backoff BackoffManager
if c.createBackoffMgr != nil {
backoff = c.createBackoffMgr()
}
if backoff == nil {
backoff = noBackoff
}
var pathPrefix string
if c.base != nil {
pathPrefix = path.Join("/", c.base.Path, c.versionedAPIPath)
} else {
pathPrefix = path.Join("/", c.versionedAPIPath)
}
var timeout time.Duration
if c.Client != nil {
timeout = c.Client.Timeout
}
r := &Request{
c: c,
rateLimiter: c.rateLimiter,
backoff: backoff,
timeout: timeout,
pathPrefix: pathPrefix,
maxRetries: 10,
}
switch {
case len(c.content.AcceptContentTypes) > 0:
r.SetHeader("Accept", c.content.AcceptContentTypes)
case len(c.content.ContentType) > 0:
r.SetHeader("Accept", c.content.ContentType+", */*")
}
return r
}
我们可以看到Get方法返回的Request对象其实是NewRequest方法构建的连接API接口的请求参数。
接着看Namespace方法:
// Namespace applies the namespace scope to a request (/[ns//])
func (r *Request) Namespace(namespace string) *Request {
if r.err != nil {
return r
}
if r.namespaceSet {
r.err = fmt.Errorf("namespace already set to %q, cannot change to %q", r.namespace, namespace)
return r
}
if msgs := IsValidPathSegmentName(namespace); len(msgs) != 0 {
r.err = fmt.Errorf("invalid namespace %q: %v", namespace, msgs)
return r
}
r.namespaceSet = true
r.namespace = namespace
return r
}
这里其实是编辑API请求中的命名空间参数,因此我们可以猜出Resource、VersionParams、Timeout、Do都是用来设置请求参数的,Into则返回请求结果。
第二个需要注意的点是,在源码的pod.go文件中有一个PodInterface接口,在这个接口中定义了pod进行增删改查的方法,也是我们需要关注的点,通过这些方法,我们就可以轻松的管理k8s集群中的pod了。
// PodInterface has methods to work with Pod resources.
type PodInterface interface {
Create(ctx context.Context, pod *v1.Pod, opts metav1.CreateOptions) (*v1.Pod, error)
Update(ctx context.Context, pod *v1.Pod, opts metav1.UpdateOptions) (*v1.Pod, error)
UpdateStatus(ctx context.Context, pod *v1.Pod, opts metav1.UpdateOptions) (*v1.Pod, error)
Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error
DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error
Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Pod, error)
List(ctx context.Context, opts metav1.ListOptions) (*v1.PodList, error)
Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error)
Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.Pod, err error)
GetEphemeralContainers(ctx context.Context, podName string, options metav1.GetOptions) (*v1.EphemeralContainers, error)
UpdateEphemeralContainers(ctx context.Context, podName string, ephemeralContainers *v1.EphemeralContainers, opts metav1.UpdateOptions) (*v1.EphemeralContainers, error)
PodExpansion
}
从eample中我们可以看到与k8s建立连接的步骤如下:
在后面的博客中我会和大家一起学习如何使用client-go管理k8s集群的pod和node,希对你有所帮助。