欢迎访问我的GitHub
这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
本篇概览
- 尽管长篇系列《client-go实战》的内容足够丰富,然而内容太多每个知识点也有一定深度,对于打算快速学习并开始kubernetes开发的新手并不友好,因此本篇的目标读者就是client-go初学者,重点解决两个基础问题:
- 如何编码操作kubernetes?
- 对应的单元测试代码怎么写,运行单元测试时可是没有kubernetes环境的,这时咱们写的那些操作kubernetes的代码能运行吗?
- 注意一:本篇写的代码是Go语言
- 注意二:文末有源码下载地址,对应本篇的完整工程源码
环境信息
- 以下是本篇实战涉及的软件的版本信息,作为您的参考
- go:1.19.3
- kubernetes:1.22.8
- client-go:v0.22.8
- 开发机:Ubuntu 20.04.4 LTS
- 编码环境:Windows 11 家庭中文版 + vs code 1.79.2
- 这里顺便提一下,编码环境不重要,我这里使用vs code的Remote Explorer插件远程连接到开发机上进行操作,也就是说写代码用的是windows编码环境,实际编译和运行都在开发机Ubuntu上面,如下图
- 本篇的主题是编码操作kubernetes,因此请确保kubernetes环境已经就绪
如何编码操作kubernetes?
- 想要编码操作kubernetes,需要使用client-go库,因此本篇主要演示的就是如何使用该库
- 首先要确定client-go的版本,这和您自己的kubernetes环境有关,在确定了kubernetes版本后如何确定client-go的版本呢?来看client-go官方说明,如下图
- 简单解释一下如何确定版本
- client-go的版本一共有两类:旧版的kubernetes-1.x.y和新版v0.x.y
- 如果kubernetes版本大于或等于1.17.0,client-go版本请选择新版,举例:如果kubernetes版本是1.20.4,client-go版本就是v0.20.4
- 如果kubernetes版本小于1.17.0,client-go版本请选择旧版,举例:如果kubernetes版本是1.20.4,client-go版本就是kubernetes-1.16.3
- 综上所述,本文使用:kubernetes:1.22.8和client-go:v0.22.8的组合
方案设计
- 正式编码前先说清楚要开发的内容,整体架构如下:
- 下面的具体的步骤:
- 开发一个web服务,名为client-go-unit-tutorials,基于gin框架
- 提供一个接口query_pods_by_label_app,作用是根据namespace和label的值,查询出符合条件的所有pod的名称
- 上述接口的具体实现用到了client-go库,使用库中的api去kubernetes的api-server查找pod,将结果的name作为接口的返回值,返回给请求方
- client-go库要想成功访问kubernetes,必须要有kubernetes环境的.kube/config文件,这里为了省事儿,直接将web服务部署到kubernetes环境的机器上,这样就能直接访问.kube/config文件了
- 编写单元测试代码,在没有kubernetes环境的情况下,也能成功执行那段操作kubernetes的代码
- 再次提醒:client-go-unit-tutorials可以在一个独立的机器上运行,也能直接运行在kubernetes机器上,还能做成镜像运行在kubernetes环境
- 接下来开始编码吧
编码:准备工程
- 执行命令名为go mod init client-go-unit-tutorials,新建module
- 确保您的goproxy是正常的
- 执行命令go get -u github.com/gin-gonic/gin,下载gin
- 执行命令go get k8s.io/client-go@v0.22.8,下载client-go的指定版本
- 现在工程已经准备好了,接着就是具体的编码,我们先从最核心的开始:操作kubernetes
编码:操作kubernetes
- 新建文件夹kubernetes_service,在里面新增文件kube.go,这是集中了kubernetes操作的代码,内容如下,这里面有几处要注意的地方,稍后会提到
package kubernetesservice
import("context""flag""log""path/filepath""sync""k8s.io/client-go/kubernetes""k8s.io/client-go/tools/clientcmd""k8s.io/client-go/util/homedir"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1""k8s.io/apimachinery/pkg/labels""k8s.io/apimachinery/pkg/selection")var CLIENT_SET kubernetes.Interface
var ONCE sync.Once
// DoInit Indexer相关的初始化操作,这里确保只执行一次funcDoInit(){
ONCE.Do(initInKubernetesEnv)}// GetClient 调用此方法返回clientSet对象funcGetClient() kubernetes.Interface {return CLIENT_SET
}// SetClient 可以通过initInKubernetesEnv在kubernetes初始化,如果有准备好的clientSet,也可以调用SetClient直接设置,而无需初始化funcSetClient(clientSet kubernetes.Interface){
CLIENT_SET = clientSet
}// initInKubernetesEnv 这里是真正的初始化逻辑funcinitInKubernetesEnv(){
log.Println("开始初始化Indexer")var kubeconfig *string// 试图取到当前账号的家目录if home := homedir.HomeDir(); home !=""{// 如果能取到,就把家目录下的.kube/config作为默认配置文件
kubeconfig = flag.String("kubeconfig", filepath.Join(home,".kube","config"),"(optional) absolute path to the kubeconfig file")}else{// 如果取不到,就没有默认配置文件,必须通过kubeconfig参数来指定
kubeconfig = flag.String("kubeconfig","","absolute path to the kubeconfig file")}// 加载配置文件
config, err := clientcmd.BuildConfigFromFlags("",*kubeconfig)if err !=nil{panic(err.Error())}// 用clientset类来执行后续的查询操作
CLIENT_SET, err = kubernetes.NewForConfig(config)if err !=nil{panic(err.Error())}
log.Println("kubernetes服务初始化成功")}// QueryPodNameByLabelApp 根据指定的namespace和label值搜索funcQueryPodNameByLabelApp(context context.Context, namespace, app string)([]string,error){
log.Printf("QueryPodNameByLabelApp, namespace [%s], app [%s]", namespace, app)
equalRequirement, err := labels.NewRequirement("app", selection.Equals,[]string{app})if err !=nil{returnnil, err
}
selector := labels.NewSelector().Add(*equalRequirement)// 查询pod列表
pods, err := CLIENT_SET.CoreV1().Pods(namespace).List(context, metav1.ListOptions{// 传入的selector在这里用到
LabelSelector: selector.String(),})if err !=nil{returnnil, err
}
names :=make([]string,0)for_, v :=range pods.Items {
names =append(names, v.GetName())}return names,nil}// CreateNamespace 单元测试的辅助工具,用于创建namespacefuncCreateNamespace(context context.Context, client kubernetes.Interface, name string)error{
namespaceObj :=&v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: name,},}_, err := client.CoreV1().Namespaces().Create(context, namespaceObj, metav1.CreateOptions{})return err
}// DeleteeNamespace 单元测试的辅助工具,用于创建namespacefuncDeleteNamespace(context context.Context, client kubernetes.Interface, name string)error{
err := client.CoreV1().Namespaces().Delete(context, name, metav1.DeleteOptions{})return err
}/*
// CreateDeployment 单元测试的辅助工具,用于创建namespace
func CreateDeployment(context context.Context, client kubernetes.Interface, namespace, name, image, app string, replicas int32) error {
_, err := client.AppsV1().Deployments(namespace).Create(context, &apps.Deployment{
TypeMeta: metav1.TypeMeta{
Kind: "Deployment",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Labels: map[string]string{
"app": app,
},
},
Spec: apps.DeploymentSpec{
Replicas: &replicas,
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Image: image,
},
},
},
},
},
}, metav1.CreateOptions{})
return err
}
*/
- 上述代码要有以下几处需要注意
- 操作kubernetes需要使用Clientset对象,该对象的创建过程集中在initInKubernetesEnv方法中,先加载配置文件,再根据配置文件创建Clientset
- CreateNamespace方法的作用是创建namespace,这里面有常规client-go库的使用方法,即Clientset的api的一些操作,以及一些资源对象的初始化
- QueryPodNameByLabelApp方法实现了核心业务功能,稍微有点复杂,在查找pod对象的时候,使用label做了一次过滤
- 接下来是响应web请求的服务类basic_curd.go,代码如下,可见非常简单,就是取请求参数,再调用上面写到的api去kubernetes查询,再返回即可
package handler
import("log""net/http""github.com/gin-gonic/gin"
kubernetesservice "client-go-unit-tutorials/kubernetes_service")const(
PARAM_NAMESPACE ="namespace"
PARAM_APP ="label_app")funcQueryPodsByLabelApp(context *gin.Context){
rlt :=make(map[string]interface{})
namespace := context.DefaultQuery(PARAM_NAMESPACE,"")
app := context.DefaultQuery(PARAM_APP,"")
log.Printf("query param, namespace [%s], app [%s]", namespace, app)
names, err := kubernetesservice.QueryPodNameByLabelApp(context, namespace, app)if err !=nil{
rlt["message"]= err.Error()
context.JSON(http.StatusInternalServerError, rlt)return}
rlt["message"]="success"
rlt["names"]= names
context.JSON(http.StatusOK, rlt)}
- 为了将gin初始化逻辑封装起来好给外部调用,这里创建了/initor/customize_initor.go,内容也很简单,就是gin的路由初始化
package initor
import("github.com/gin-gonic/gin""client-go-unit-tutorials/handler")const(
PATH_QUERY_PODS_BY_LABEL_APP ="/query_pods_by_label_app")funcInitRouter()*gin.Engine {
r := gin.Default()// 绑定path的handler
r.GET(PATH_QUERY_PODS_BY_LABEL_APP, handler.QueryPodsByLabelApp)return r
}
- 最后是main.go,这里面很简单,主动调用kubernetes和gin的初始化方法
package main
import("client-go-unit-tutorials/initor"
kubernetesservice "client-go-unit-tutorials/kubernetes_service")funcmain(){// 初始化kubernetes相关配置
kubernetesservice.DoInit()
router := initor.InitRouter()_= router.Run(":18080")}
- 以上就是完整的代码了,接下来咱们把代码运行起来看看效果
运行代码前的准备工作
- 首先要在kubernetes环境把deployment部署好,如此调用查询接口才有数据返回
- 先创建namespace
kubectl create namespace client-go-tutorials
- 创建名为nginx-deployment-service.yaml的文件,内容如下
---apiVersion: apps/v1
kind: Deployment
metadata:namespace: client-go-tutorials
name: nginx-deployment
labels:app: nginx-app
type: front-end
spec:replicas:3selector:matchLabels:app: nginx-app
type: front-end
template:metadata:labels:app: nginx-app
type: front-end
# 这是第一个业务自定义label,指定了mysql的语言类型是c语言language: c
# 这是第二个业务自定义label,指定了这个pod属于哪一类服务,nginx属于web类business-service-type: web
spec:containers:-name: nginx-container
image: nginx:latest
resources:limits:cpu:"0.5"memory: 128Mi
requests:cpu:"0.1"memory: 64Mi
---apiVersion: v1
kind: Service
metadata:namespace: client-go-tutorials
name: nginx-service
spec:type: NodePort
selector:app: nginx-app
type: front-end
ports:-port:80targetPort:80nodePort:30011
- 执行以下脚本完成部署
kubectl apply -f nginx-deployment-service.yaml
- 稍后会创建三个nginx的pod,接下来咱们就要用代码来查询这些pod了
kubectl get pods -n client-go-tutorials
NAME READY STATUS RESTARTS AGE
nginx-deployment-78f6b696d9-j98xj 1/1 Running 0 19h
nginx-deployment-78f6b696d9-wp4qf 1/1 Running 0 7d17h
nginx-deployment-78f6b696d9-wpnt7 1/1 Running 0 20h
运行代码
- 正常情况下,应该是执行go build编译项目,得到名为client-go-unit-tutorials的可执行文件,部署在可以访问kubernetes的机器上运行
- 我这边开发机上就部署着kubernetes,因此,只要在vscode上运行项目就行了,运行应用的配置文件launch.json,如下
{"version":"0.2.0","configurations":[{"name":"Launch Package","type":"go","request":"launch","mode":"auto","program":"${workspaceFolder}"}]}
- 调用请求的方法很多,postman、curl命令都可以,我这里用的是vscode的REST Client插件,可以把请求以脚本的方式保存下来,脚本如下
### 变量
@namespace=client-go-tutorials
@label_app=nginx-app
### 测试用例,指定namespace和label查询所有的pod名称
GET http://192.168.50.76:18080/query_pods_by_label_app?namespace={{namespace}}&label_app={{label_app}}
- 点击下图红色箭头所指的Send Request就会发送请求
- 收到响应如下,可见所有符合要求的pod的name都在响应body中了
HTTP/1.1 200 OK
Content-Type: application/json;charset=utf-8
Date: Sun, 02 Jul 2023 04:58:03 GMT
Content-Length: 139
Connection: close
{"message":"success",
"names":["nginx-deployment-78f6b696d9-j98xj",
"nginx-deployment-78f6b696d9-wp4qf",
"nginx-deployment-78f6b696d9-wpnt7"]}
- 至此,整篇内容已经完成了二分之一,接下里要看的就是如何编写单元测试代码了,要在一个没有kubernetes的环境下成功运行操作kubernetes的代码
关键知识点:使用client-go库的代码如何写单元测试
- 如果您只想了解client-go有关的单元测试的关键知识点,对其他内容不感兴趣,下面黄色箭头所指这行代码足够了,在单元测试中使用fake.NewSimpleClientset()创建的clientset,只要运行单元测试时应用代码用到的是这个clientset,就可以和实际kubernetes环境使用clientset一样了,创建的资源也能被查出来
- 打开上图中的NewSimpleClientset方法,看看它创建的clientset是何方神圣,如下图,这个fake包下面的Clientset,已经把kubernetes.Interface接口完整实现了,在单元测试中可以用来取代正式环境中调用kubernetes.NewForConfig创建的clentset对象
- 以上解答了单元测试时如何脱离kubernetes环境使用client-go库的问题,这只是一个技术点而已,接下来咱们把完整的单元测试代码写出来
编码:单元测试
- 首先是辅助工具,这里面有多个方法,都是辅助单元测试的,例如SingleTest方法可以用来发送请求并将响应返回,Check方法可以检查返回内容等
package unittesthelper
import("context""encoding/json""fmt""log""net/http""net/http/httptest""github.com/gin-gonic/gin""github.com/stretchr/testify/suite"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1""k8s.io/client-go/kubernetes")const(
TEST_NAMESPACE ="client-go-tutorials"
TEST_POD_NAME_PREFIX ="nginx-pod-"
TEST_IMAGE ="nginx:latest"
TEST_LABEL_APP ="nginx-app"
TEST_POD_NUM =3)// 数据结构,用于保存web响应的bodytype ResponseNames struct{
Message string`json:"message"`
Names []string`json:"names"`}// SingleTest 辅助方法,发请求,返回响应funcSingleTest(router *gin.Engine, url string)(int,string,error){
log.Printf("start SingleTest, request url : %s", url)
w := httptest.NewRecorder()
req,_:= http.NewRequest(http.MethodGet, url,nil)
router.ServeHTTP(w, req)return w.Code, w.Body.String(),nil}// 9. 辅助方法,解析web响应,检查结果是否符合预期funcCheck(suite *suite.Suite, body string, expectNum int){
suite.NotNil(body)
response :=&ResponseNames{}
err := json.Unmarshal([]byte(body), response)if err !=nil{
log.Fatalf("unmarshal response error, %s", err.Error())}
suite.EqualValues(expectNum,len(response.Names))}// CreatePodObj 辅助方法,用于创建pod对象funcCreatePodObj(namespace, name, app, image string)*v1.Pod {return&v1.Pod{
TypeMeta: metav1.TypeMeta{
Kind:"Deployment",
APIVersion:"apps/v1",},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Labels:map[string]string{"app": app,},},
Spec: v1.PodSpec{
Containers:[]v1.Container{{
Image: image,},},},}}// CreateDeployment 单元测试的辅助工具,用于创建namespacefuncCreatePods(context context.Context, client kubernetes.Interface, namespace, name, image, app string)error{_, err := client.CoreV1().Pods(namespace).Create(context,CreatePodObj(namespace, name, app, image), metav1.CreateOptions{})return err
}// CreatePod 辅助方法,用于创建多个podfuncCreatePod(context context.Context, client kubernetes.Interface, num int){for i :=0; i < num; i++{if err :=CreatePods(context,
client,
TEST_NAMESPACE,
fmt.Sprintf("%s%d", TEST_POD_NAME_PREFIX, i),
TEST_IMAGE,
TEST_LABEL_APP); err !=nil{
log.Fatalf("create pod [%d] error, %s", i, err.Error())}}}
- 接下来就是完整的单元测试代码,这里面是一个常规的单元测试集的开发,注释中用数字标明了每一步的执行顺序,按部就班完成就好,要注意的是SetupTest方法,里面用mock出来的clientset创建了三个pod,这些pod在查询的时候是可以被查出来的,有了这个mock版的clientset的帮助,就算没有kubernetes环境,咱们的代码照样能正常运行
package handler_test
import("client-go-unit-tutorials/handler""client-go-unit-tutorials/initor"
kubernetesservice "client-go-unit-tutorials/kubernetes_service""client-go-unit-tutorials/unittesthelper""context""fmt""log""net/http""testing""github.com/gin-gonic/gin""github.com/stretchr/testify/suite""k8s.io/client-go/kubernetes""k8s.io/client-go/kubernetes/fake")// 1. 定义suite数据结构type MySuite struct{
suite.Suite
ctx context.Context
cancel context.CancelFunc
clientSet kubernetes.Interface
router *gin.Engine
}// 2. 单元测试的初始化操作func(mySuite *MySuite)SetupTest(){
client := fake.NewSimpleClientset()
kubernetesservice.SetClient(client)
mySuite.ctx, mySuite.cancel = context.WithCancel(context.Background())
mySuite.clientSet = client
mySuite.router = initor.InitRouter()// 初始化数据,创建namespaceif err := kubernetesservice.CreateNamespace(mySuite.ctx, client, unittesthelper.TEST_NAMESPACE); err !=nil{
log.Fatalf("create namespace error, %s", err.Error())}// 初始化数据,创建pod
unittesthelper.CreatePod(mySuite.ctx, client,3)}// 3. 定义测试完成后的收尾工作,例如清理一些资源func(mySuite *MySuite)TearDownTest(){// 删除namespaceif err := kubernetesservice.DeleteNamespace(mySuite.ctx, kubernetesservice.GetClient(), unittesthelper.TEST_NAMESPACE); err !=nil{
log.Fatalf("delete namespace error, %s", err.Error())}
mySuite.cancel()}// 4. 启动测试集funcTestBasicCrud(t *testing.T){
suite.Run(t,new(MySuite))}// 5. 定义测试集func(mySuite *MySuite)TestBasicCrud(){// 5.1 若有需要,执行monkey.Patch// 5.2 若执行了monkey.Patch,需要执行defer monkey.UnpatchAll()// 5.3 执行单个测试// 参考 client-go/examples/fake-client/main_test.go/main_test.go
mySuite.Run("常规查询",func(){
url := fmt.Sprintf("%s?%s=%s&%s=%s",
initor.PATH_QUERY_PODS_BY_LABEL_APP,
handler.PARAM_NAMESPACE,
unittesthelper.TEST_NAMESPACE,
handler.PARAM_APP,
unittesthelper.TEST_LABEL_APP)
code, body,error:= unittesthelper.SingleTest(mySuite.router, url)iferror!=nil{
mySuite.Fail("SingleTest error, %v",error)return}// 检查返回码
mySuite.EqualValues(http.StatusOK, code)// 检查结果
unittesthelper.Check(&mySuite.Suite, body, unittesthelper.TEST_POD_NUM)})}
- 点击下图黄色箭头所指按钮,即可开始单元测试
- 得到结果如下,在没有kubernetes环境的情况下,单元测试通过,所有操作kubernetes的代码均能正常运行
=== RUN TestBasicCrud
=== RUN TestBasicCrud/TestBasicCrud
[GIN-debug][WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug][WARNING] Running in"debug" mode. Switch to "release" mode in production.
- using env: exportGIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)[GIN-debug] GET /query_pods_by_label_app --> client-go-unit-tutorials/handler.QueryPodsByLabelApp (3 handlers)=== RUN TestBasicCrud/TestBasicCrud/常规查询
2023/07/02 05:17:27 start SingleTest, request url : /query_pods_by_label_app?namespace=client-go-tutorials&label_app=nginx-app
2023/07/02 05:17:27 query param, namespace [client-go-tutorials], app [nginx-app]2023/07/02 05:17:27 QueryPodNameByLabelApp, namespace [client-go-tutorials], app [nginx-app][GIN]2023/07/02 - 05:17:27 |200|205.281µs || GET "/query_pods_by_label_app?namespace=client-go-tutorials&label_app=nginx-app"
--- PASS: TestBasicCrud/TestBasicCrud/常规查询 (0.00s)
--- PASS: TestBasicCrud/TestBasicCrud (0.00s)
--- PASS: TestBasicCrud (0.00s)
PASS
ok client-go-unit-tutorials/handler 0.034s
> Test run finished at 7/2/2023, 1:17:26 PM <
- 至此,client-go初级篇已经完成,希望能对刚刚涉及kubernetes开发的读者有所帮助
源码下载
如果您不想编写代码,也可以从GitHub上直接下载,地址和链接信息如下表所示(https://github.com/zq2599/blog_demos):
名称链接备注项目主页https://github.com/zq2599/blog_demos该项目在GitHub上的主页git仓库地址(https)https://github.com/zq2599/blog_demos.git该项目源码的仓库地址,https协议git仓库地址(ssh)git@github.com:zq2599/blog_demos.git该项目源码的仓库地址,ssh协议这个git项目中有多个文件夹,本篇的源码在tutorials/client-go-unit-tutorials文件夹下,如下图红框所示:
你不孤单,欣宸原创一路相伴
- Java系列
- Spring系列
- Docker系列
- kubernetes系列
- 数据库+中间件系列
- DevOps系列
版权归原作者 程序员欣宸 所有, 如有侵权,请联系我们删除。