在了解 Client-go 之前需要先理解一个概念叫 RESTful API
RESTful API
在主流公司的程序开发中,为了提高程序开发迭代的速度,基本都是前后端分离架构,而前端既包括网页、App、小程序等等,因此必须要有一个统一的规范用于约束前后端的通信,RESTful API则是目前比较成熟的API设计理论。
REST(Representational State Transfer):表现层状态转移
RESTful是一种风格而不是标准,而这个风格大致有以下几个主要特征:
以资源为基础 :资源可以是一个图片、音乐、一个XML格式、HTML格式或者JSON格式等网络上的一个实体,除了一些二进制的资源外普通的文本资源更多以JSON为载体、面向用户的一组数据(通常从数据库中查询而得到)。
统一接口: 对资源的操作包括获取、创建、修改和删除,这些操作正好对应HTTP协议提供的GET、POST、PUT和DELETE方法。换言而知,使用RESTful风格的接口但从接口上你可能只能定位其资源,但是无法知晓它具体进行了什么操作,需要具体了解其发生了什么操作动作要从其HTTP请求方法类型上进行判断。具体的HTTP方法和方法含义如下:
GET(SELECT):从服务器取出资源(一项或多项)。
POST(CREATE):在服务器新建一个资源。
PUT(UPDATE):在服务器更新资源(客户端提供完整资源数据)。
PATCH(UPDATE):在服务器更新资源(客户端提供需要修改的资源数据)。
DELETE(DELETE):从服务器删除资源。
简单概括就是:RESTful 设计风格 可以让用户通过HTTP加不同的操作方式实现对资源的增删查改。
Client-go
Kubernetes就是经典的 RESTful 架构,用户可以轻松的通过GET、POST等方式访问、操作Kubernetes中的资源,例如Deployments、Pods、namespaces等。包括Kubectl命令其本质也是通过HTTP请求 kubernetes API Server 实现对资源的操作
通过 RESTful API 将前端和后端分离,用户可以直接的通过 HTTP 协议提交表单来对操作资源。但是频繁的填写表单对于开发人员来说并不是一种高效的的方式。
于是client-go应运而生(除此之位还有client-java、client-python等),client-go中封装了HTTP连接,使开发人员可以更高效的操作资源。
client-go中一共有四种客户端,分别是:
RestClient:
RestClient是最基础的客户端,其结构中包含 HTTP连接、URL链接、GroupVersion、以及编解码方式等参数。
使用RestClient之前需要了解几个基本的概念,以Deployment资源为例:
假设Deployment资源的路径是:
http://192.168.1.1:8001/apis/apps/v1/deployments
http://192.168.1.1:8001 就是 kubernetes 的 API Server 暴露出来的一个服务地址,通过这个服务地址,用户便可以和 API Server 建立连接。RestClient中的Client便相当于行驶建立连接这个过程,当然,Client还需要经过鉴权、认证等操作才可以建立连接,这里只是类比。
/apis 是最基础的api路径,除了apis外,还有api这个路径,pod、node、namespace等资源便在api路径下。
/apps/v1 这里的apps则是资源的Group,v1则是资源的Version,他们一起组成了GroupVersion,并注册到了Scheme中。
下面给出两个demo,分别是通过 RestClient 部署、删除namespace和deployment
func restOptNs() {
var kubeconfig = "{KUBECONFIG PATH}"
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
panic(err.Error())
}
// 参考path : /api/v1/namespaces/{namespace}
config.APIPath = "api"
config.GroupVersion = &corev1.SchemeGroupVersion
config.NegotiatedSerializer = scheme.Codecs
restClient, err := rest.RESTClientFor(config)
if err != nil {
panic(err)
}
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test-client-go",
},
Spec: corev1.NamespaceSpec{},
}
result := &corev1.Namespace{}
err = restClient.Post().
Resource("namespaces").
Body(ns).
Do(context.TODO()).
Into(result)
if err != nil {
panic(err)
}
fmt.Printf("namespace %s created !\n", ns.Name)
time.Sleep(time.Second * 20)
err = restClient.Delete().
Resource("namespaces").
Name(ns.Name).
Do(context.TODO()).
Error()
if err != nil {
panic(err)
}
fmt.Printf("namespace %s deleted !\n", ns.Name)
}
func restK8sDeploy() {
var kubeconfig = "{KUBECONFIG PATH}
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
panic(err.Error())
}
// 参考path : /api/apps/v1/namespaces/{namespace}/deployments
config.APIPath = "apis"
config.GroupVersion = &appsv1.SchemeGroupVersion
config.NegotiatedSerializer = scheme.Codecs
restClient, err := rest.RESTClientFor(config)
if err != nil {
panic(err)
}
deploy := &appsv1.Deployment{}
bytes, err := ioutil.ReadFile("./yamls/nginx.yaml")
if err != nil {
panic(err)
}
toJson, err := yaml.ToJSON(bytes)
if err != nil {
panic(err)
}
json.Unmarshal(toJson, deploy)
var replicas int32 = 1
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "nginx",
Labels: map[string]string{
"app": "nginx",
},
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "nginx",
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Name: "nginx",
Labels: map[string]string{
"app": "nginx",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "nginx",
Image: "nginx:latest",
Ports: []corev1.ContainerPort{
{
Name: "http",
Protocol: corev1.ProtocolTCP,
ContainerPort: 80,
},
},
},
},
},
},
},
}
err = restClient.Delete().
Namespace(deploy.ObjectMeta.Namespace).
Resource("deployments").
Name(deploy.ObjectMeta.Name).
Do(context.TODO()).
Error()
if err != nil {
panic(err)
}
fmt.Printf("deployment %s deleted !\n", deploy.Name)
}
ClientSet
通过上文中的两个例子得知,在连接RestClient之前,需要配置基础路径(api、apis)以及资源的GroupVersion和编解码的方式,对于不同的资源(属于不同的GroupVersion)需要不同的配置,操作还是过于繁琐。
于是便出现了ClientSet,ClietSet实现了对RestClient的再封装。
其结构体中涵盖了所有的k8s资源,通过ClientSet,开发人员只需要建立一次连接,便可以通过不同的接口操作k8s中不同的资源,不用再重新定义GroupVersion、再重新建立连接。
func cliensetK8s() {
var kubeconfig = "{KUBECONFIG PATH}"
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
panic(err.Error())
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err.Error())
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second*100))
defer cancel()
listPod(clientset, ctx, "")
}
// 获取 Pod 列表
func listPod(clientset *kubernetes.Clientset, ctx context.Context, ns string) {
pods, err := clientset.CoreV1().Pods("").List(ctx, metav1.ListOptions{})
if err != nil {
panic(err.Error())
}
fmt.Printf("There are %d pods in the k8s cluster\n", len(pods.Items))
// 获取指定 namespace 中的 Pod 列表信息
namespace := ns
pods, err = clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
panic(err)
}
fmt.Printf("\nThere are %d pods in namespaces %s\n", len(pods.Items), namespace)
for _, pod := range pods.Items {
// fmt.Printf("Name: %s, NameSpace: %s, Status: %s, CreateTime: %s\n", pod.ObjectMeta.Name, pod.ObjectMeta.Namespace, pod.Status.Phase, pod.ObjectMeta.CreationTimestamp)
listPodInfo(clientset, ctx, pod.ObjectMeta.Namespace, pod.ObjectMeta.Name)
}
}
func listPodInfo(clientset *kubernetes.Clientset, ctx context.Context, ns string, podName string) {
namespace := ns
pod, err := clientset.CoreV1().Pods(namespace).Get(ctx, podName, metav1.GetOptions{})
if errors.IsNotFound(err) {
fmt.Printf("Pod %s in namespace %s not found\n", podName, namespace)
} else if statusError, isStatus := err.(*errors.StatusError); isStatus {
fmt.Printf("Error getting pod %s in namespace %s: %v\n", podName, namespace, statusError.ErrStatus.Message)
} else if err != nil {
panic(err.Error())
} else {
fmt.Printf("\nFound pod %s in namespace %s\n", podName, namespace)
maps := map[string]interface{}{
"Name": pod.ObjectMeta.Name,
"Namespaces": pod.ObjectMeta.Namespace,
"NodeName": pod.Spec.NodeName,
"Annotations": pod.ObjectMeta.Annotations,
"Labels": pod.ObjectMeta.Labels,
"Uid": pod.ObjectMeta.UID,
"Status": pod.Status.Phase,
"IP": pod.Status.PodIP,
"Image": pod.Spec.Containers[0].Image,
}
prettyPrint(maps)
}
}
在这里:
pods, err := clientset.CoreV1().Pods("").List(ctx, metav1.ListOptions{})
Pod 的 GroupVersion 便已经封装到了 .CoreV1() 接口中,从而简化了连接配置
DynamicClient
DynamicClient是一种动态客户端它可以对任何资源进行restful操作包括crd自定义资源,不同于 clientset,dynamic client 返回的对象是一个 map[string]interface{},如果一个 controller 中需要控制所有的 API,可以使用dynamic client,目前它在 garbage collector 和 namespace controller中被使用。
DynamicClient的处理过程将Resource例如podlist转换为unstructured类型,k8s的所有resource都可以转换为这个结构类型,处理完之后在转换为podlist,整个转换过程类似于接口转换就是通过interface{}的断言。
Dynamic client 是一种动态的 client,它能处理 kubernetes 所有的资源,只支持JSON。
func dynamicK8s() {
var kubeconfig = "{KUBECONFIG PATH}"
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
panic(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
panic(err)
}
// 定义组版本资源
gvr := schema.GroupVersionResource{Version: "v1", Resource: "pods"}
unStructObj, err := dynamicClient.Resource(gvr).Namespace(namespace).List(metav1.ListOptions{})
if err != nil {
panic(err)
}
podList := &apiv1.PodList{}
if err = runtime.DefaultUnstructuredConverter.FromUnstructured(unStructObj.UnstructuredContent(), podList); err != nil {
panic(err)
}
for _, v := range podList.Items {
fmt.Printf("namespaces:%v name:%v status:%v \n", v.Namespace, v.Name, v.Status.Phase)
}
}
DiscoverClient
DiscoveryClient是发现客户端,主要用于发现api server支持的资源组 资源版本 资源信息.
func discover() {
var kubeconfig = "{KUBECONFIG PATH}"
// 从本机加载kubeconfig配置文件,因此第一个参数为空字符串
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
// kubeconfig加载失败就直接退出了
if err != nil {
panic(err.Error())
}
// 新建discoveryClient实例
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
panic(err.Error())
}
// 获取所有分组和资源数据
APIGroup, APIResourceListSlice, err := discoveryClient.ServerGroupsAndResources()
if err != nil {
panic(err.Error())
}
// 先看Group信息
fmt.Printf("APIGroup :\n\n %v\n\n\n\n", APIGroup)
// APIResourceListSlice是个切片,里面的每个元素代表一个GroupVersion及其资源
for _, singleAPIResourceList := range APIResourceListSlice {
// GroupVersion是个字符串,例如"apps/v1"
groupVerionStr := singleAPIResourceList.GroupVersion
// ParseGroupVersion方法将字符串转成数据结构
gv, err := schema.ParseGroupVersion(groupVerionStr)
if err != nil {
panic(err.Error())
}
fmt.Println("*****************************************************************")
fmt.Printf("GV string [%v]\nGV struct [%#v]\nresources :\n\n", groupVerionStr, gv)
// APIResources字段是个切片,里面是当前GroupVersion下的所有资源
for _, singleAPIResource := range singleAPIResourceList.APIResources {
fmt.Printf("%v\n", singleAPIResource.Name)
}
}
}
至此,四种Client-go客户端就已经介绍完毕