> 文章列表 > k8s client-go 程序实现kubernetes Controller Operator 使用CRD 学习总结

k8s client-go 程序实现kubernetes Controller Operator 使用CRD 学习总结

k8s client-go 程序实现kubernetes Controller  Operator 使用CRD 学习总结

k8s client-go 程序实现kubernetes Controller & Operator 使用CRD 学习总结

大纲

  • 1 定义CRD
  • 2 client-go自动代码生成
  • 3 client-go操作CR
  • 4 创建镜像
  • 5 配置权限
  • 6 部署到k8s

基础流程

这里使用client-go实现编写,相对于kubebuiler这些工具生成脚手架工程要麻烦一些,但是可以理解完整的原理。

k8s 自定义operator 基本流程

  • 1 定义crd
  • 2 使用code-generator 生成自定义clientset
  • 3 编写代码
  • 4 配置权限
  • 5 发布到k8s集群

定义CRD

此例子中使用的CRD自定义资源定义基本和 《k8s java程序实现kubernetes Controller & Operator 使用CRD 学习总结》 文章中使用的CRD一致

CRD自定义资源定义yaml文件内容如下 (yaml/crd-liuyijiang.yaml )

# 定义自定义的 MyCrdGolangTest 资源
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:# 名字必需与下面的 spec 字段匹配,并且格式为 '<名称的复数形式>.<组名>'name: mycrdgolangtests.liuyjiang.comspec:   # 组名称,用于 REST API: /apis/<组>/<版本>group: liuyjiang.comnames:# 名称的复数形式,用于 URL:/apis/<组>/<版本>/<名称的复数形式>plural: mycrdgolangtests# 名称的单数形式,作为命令行使用时和显示时的别名singular: mycrdgolangtest# kind 通常是单数形式的驼峰命名(CamelCased)形式。你的资源清单会使用这一形式。kind: MyCrdGolangTest# shortNames 允许你在命令行使用较短的字符串来匹配资源shortNames:- mcgt# 可以是 Namespaced 或 Cluster   scope: Namespaced       versions:- name: v1# 每个版本都可以通过服务标志启用/禁用。served: true# 必须将一个且只有一个版本标记为存储版本。storage: trueschema:openAPIV3Schema:type: objectproperties:  #自定义CRD中的specspec:type: objectproperties:# 自定义的资源spec 中的属性 mymsgmymsg: type: string# 自定义的资源spec 中的属性 myarray  myarray:    type: arrayitems:type: string # 自定义的资源spec 中的属性 mynumber         mynumber:  type: integer#自定义CRD中的status    status:type: object    properties:  mystatus: type: stringmyip: type: string  

资源定义完成后使用 kubectl apply -f crd-liuyijiang.yaml 在集群内部先创建好CRD

kubectl apply -f crd-liuyijiang.yaml
kubectl get crd mycrdgolangtests.liuyjiang.com

在这里插入图片描述

crd 创建成功

kubectl describe crd mycrdgolangtests.liuyjiang.com 查看crd内容

在这里插入图片描述

client-go code-generator 自动代码生成

如果不选择使用自动代码生成,可以直接使用client-go 提供的 dynamicClient实现操作CR,但是使用起来相对麻烦

例如需要使用 unstructured.Unstructured实现创建资源

在这里插入图片描述

需要使用 watch方法实现监听,无法使用Informer

在这里插入图片描述

使用 code-generator 可以生成对应的clientset, Informer, lister等编码时更方便

code-generator地址:
https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/code-generator

测试时发现code-generator 生成代码需要在linux环境下才能正常运行,在window环境下生成会有如下错误 unknown escape sequence(and 3 more errors) 或者 illegal character U+005C ‘’ (and 14 more errors)

errors in package "crdtest\\\\pkg\\\\client\\\\clientset\\\\versioned":
unable to format file "..\\\\crdtest\\\\pkg\\\\client\\\\clientset\\\\versioned\\\\clientset.go" (23:24: unknown escape sequence (and 3 more errors)).errors in package "crdtest\\\\pkg\\\\client\\\\versioned\\\\typed\\\\liuyijiang.com\\\\v1":
unable to format file "..\\\\crdtest\\\\pkg\\\\client\\\\versioned\\\\typed\\\\liuyijiang.com\\\\v1\\\\mycrdgolangtest.go" (27:9: illegal character U+005C '\\' (and 14 more errors)).

使用gitbash 报错

在这里插入图片描述

使用 cygwin 也报错

在这里插入图片描述

看生成的代码,是文件分割符号有问题 目前还未解决此问题!所以只有在linux环境下执行code-generator 脚本生成代码

client-go code-generator 生成代码流程

基本流程:

  • 1 需要一台配置了go环境的linux系统机器
  • 2 安装code-generator 代码生成工具
  • 3 创建一个最简单的go项目
  • 4 go项目需要使用git管理 (否则报错 fatal: not a git repository (or any of the parent directories): .git)
  • 5 编写 doc.go types.go register.go 文件用于生成代码

linux搭建go环境

golang 下载地址 https://golang.google.cn/dl/

本次测试使用golang 版本 go1.19.3.linux-amd64.tar.gz

例如在把 golang 安装到 /ops/go

在这里插入图片描述

修改环境变量 path

vi /etc/profile把go命令和gopath 添加到环境变量中
export PATH=/ops/go/go/bin:$PATH
export GOPATH=/root/gosource /etc/profile

在这里插入图片描述

配置代理和开启go mod

go env -w GO111MODULE=on  
go env -w GOPROXY=https://goproxy.cn,direct

在这里插入图片描述

进入到/root 文件夹 目前还没有go文件夹 手动创建

mkdir -p ./go/pkg

在这里插入图片描述

此时 linux go环境搭建完成

安装 code-generator

code-generator
官方地址 https://github.com/kubernetes/code-generator

在 $GOPTAH/pkg (即/root/go/pkg)文件夹下拉取code-generator

git clone https://github.com/kubernetes/code-generator

在这里插入图片描述

进入code-generator安装代码生成需要的工具

go install ./cmd/{applyconfiguration-gen,client-gen,deepcopy-gen,informer-gen,lister-gen}

在这里插入图片描述

当执行generate-groups.sh all 时会分别调用applyconfiguration-gen,client-gen,deepcopy-gen,informer-gen,lister-gen生成工具

工具说明

  • client-gen 用于生成clientset相关代码
  • deepcopy-gen 用于实现对象deepcopy
  • informer-gen 用于生成Informer相关代码
  • lister-gen 用于生成Lister相关代码
  • applyconfiguration-gen 生成配置相关的的代码 generate-groups.sh all 时会调用

此时code-generator 代码生产需要的环境已经完成

go项目搭建

在开发环境 window机器上的创建项目

注意:项目需要使用git管理 否则使用 deepcopy-gen生成深拷贝代码时会出现以下异常

fatal: not a git repository (or any of the parent directories): .git

step1 在gitee上创建一个项目 方便等下在linux环境下 pull push 生成后的代码

例如已经在gitee上创建好一个空的项目crdtest。并在 D:\\giteecode 拉取项目

git clone  https://gitee.com/liuyijiang/crdtest.git
cd crdtest
go mod init crdtest

在crdtest文件夹中创建 pkg/apis/liuyijiang.com/v1 文件夹用于存放代码生成需要的模板文件

其中 liuyijiang.com文件夹和自己CRM配置文件中的组名称一致

step2 添加模板代码

需要三个模板文件 doc.go types.go register.go

doc.go 内容如下

// +k8s:deepcopy-gen=package// +groupName=liuyjiang.com
package v1

+k8s:deepcopy-gen=package用来告诉生成器来生成我们自定义资源类型的deepcopy方法,+groupName=liuyjiang.com是指定我们的group名称

types.go 内容如下

package v1import (metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)// +genclient
// +genclient:noStatus
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type MyCrdGolangTest struct {metav1.TypeMeta   `json:",inline"`metav1.ObjectMeta `json:"metadata,omitempty"`Spec              MyCrdGolangTestSpec   `json:"spec"`Status            MyCrdGolangTestStatus `json:"status"`
}type MyCrdGolangTestSpec struct {Mymsg    string   `json:"mymsg"`Mynumber int64    `json:"mynumber"`Myarray  []string `json:"myarray"`
}type MyCrdGolangTestStatus struct {Mystatus string `json:"mystatus"`Myip     string `json:"myip"`
}// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object// StudentList is a list of Student resources
type MyCrdGolangTestList struct {metav1.TypeMeta `json:",inline"`metav1.ListMeta `json:"metadata"`Items []MyCrdGolangTest `json:"items"`
}

types.go主要就是编写自定义CRD结构体Spec Status等

register.go 内容如下

package v1import (metav1 "k8s.io/apimachinery/pkg/apis/meta/v1""k8s.io/apimachinery/pkg/runtime""k8s.io/apimachinery/pkg/runtime/schema"
)var SchemeGroupVersion = schema.GroupVersion{Group:   "liuyjiang.com",Version: "v1",
}var (SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)AddToScheme   = SchemeBuilder.AddToScheme
)func Resource(resource string) schema.GroupResource {return SchemeGroupVersion.WithResource(resource).GroupResource()
}func Kind(kind string) schema.GroupKind {return SchemeGroupVersion.WithKind(kind).GroupKind()
}func addKnownTypes(scheme *runtime.Scheme) error {scheme.AddKnownTypes(SchemeGroupVersion,&MyCrdGolangTest{},&MyCrdGolangTestList{},)// register the type in the schememetav1.AddToGroupVersion(scheme, SchemeGroupVersion)return nil
}

register.go 就是一个模板代码,只需要注意SchemeGroupVersion 使用自己的组名和版本名称,addKnownTypes中添加自己创建的CRD结构体指针

step3 最后执行 go mod tidy 添加依赖

使用 go mod tidy 添加依赖

在这里插入图片描述

此时项目结构如下 注意此时是 未生成代码前的项目结构

在这里插入图片描述

到此 go项目搭建完成 使用git push 到远端服务上

到linux机器上生成代码

拉取刚才提交的代码 注意此时在linux机器上,任意文件夹下都可以

进入go项目内执行生成代码脚本

/root/go/pkg/code-generator/generate-groups.sh all crdtest/pkg/client crdtest/pkg/apis liuyjiang.com:v1 --go-header-file=/root/go/pkg/code-generator/examples/hack/boilerplate.go.txt --output-base ../

在这里插入图片描述

生成命令说明:

  • /root/go/pkg/code-generator/generate-groups.sh
这里使用code-generator项目中的generate-groups.sh
  • all
是使用 applyconfiguration,client,deepcopy,informer,lister 的简写
  • crdtest/pkg/client
是生成的文件的包名,简单说就是会在 crdtest/pkg/client文件下创建生成的文件,并且包名以crdtest/pkg/client为前缀
注意这里要配合--output-base 指定输出的根路径
例如在crdtest文件夹内执行命令 那么--output-base需要指定为 ../ 即输出根路径是当前文件夹的父文件夹
  • crdtest/pkg/apis
是指定模板文件的位置,简单说就是doc.go  types.go  register.go这几个文件放置的最上层文件夹名称
  • liuyjiang.com:v1
指定组名和版本与CRD配置中的组名和版本一致即可
  • –go-header-file
直接使用code-generator项目中的现有的boilerplate.go.txt文件
/root/go/pkg/code-generator/examples/hack/boilerplate.go.txt 
  • –output-base
这里使用 ../ 即输出根路径是当前文件夹的父文件夹 ,这样输出文件就可以在pkg文件夹下了

generate-groups.sh 脚本参数与使用方式如下

在这里插入图片描述

此时代码生成完成 git push后回到window环境上查看

在这里插入图片描述

如有报错再执行一下 go mod tidy

client-go操作CR

有了生成代码后 就可以操作自定义的资源了,现在编写一个简单的web服务,可以创建并查询自定义的CRD,同时启动一个线程监听CRD

注意: 集群内部需要使用 rest.InClusterConfig() 获取权限信息

config, err := rest.InClusterConfig()if err != nil {panic(err.Error())}

相关代码如下

web.go

package crdwebimport ("context"crdv1 "crdtest/pkg/apis/liuyijiang.com/v1"crdclientset "crdtest/pkg/client/clientset/versioned""fmt""net/http""time"crdexternalversions "crdtest/pkg/client/informers/externalversions"metav1 "k8s.io/apimachinery/pkg/apis/meta/v1""k8s.io/apimachinery/pkg/util/wait""k8s.io/client-go/rest""k8s.io/client-go/tools/cache"
)func StartWeb() {go watchMyCrdGolangTest()fmt.Println("start")http.HandleFunc("/", index)http.HandleFunc("/get", get)http.HandleFunc("/list", list)http.HandleFunc("/create", create)//http.HandleFunc("/update", index)//http.HandleFunc("/delete", index)http.ListenAndServe(":8000", nil)}// 对应首页
func index(w http.ResponseWriter, r *http.Request) {w.Write([]byte("hello this is MyCrdGolangTest \\n"))}func get(w http.ResponseWriter, r *http.Request) {crdName := r.FormValue("name")w.Write([]byte(getMyCrdGolangTestStr(crdName)))}func list(w http.ResponseWriter, r *http.Request) {//crdName := r.FormValue("name")w.Write([]byte(listMyCrdGolangTest()))}func create(w http.ResponseWriter, r *http.Request) {crdName := r.FormValue("name")createMyCrdGolangTest(crdName)w.Write([]byte("createMyCrdGolangTest success \\n"))}func getClientSet() *crdclientset.Clientset {//外部访问集群的方式// config, err := clientcmd.BuildConfigFromFlags("", "./config")// if err != nil {// 	fmt.Println(err)// }//使用集群内部访问k8s的方式config, err := rest.InClusterConfig()if err != nil {panic(err.Error())}clientset, err := crdclientset.NewForConfig(config)if err != nil {fmt.Println(err)}return clientset
}/*
创建自定义资源  MyCrdGolangTest
*/
func createMyCrdGolangTest(name string) {clientset := getClientSet()namespace := "crd-golang-test"typeMeta := metav1.TypeMeta{Kind:       "MyCrdGolangTest",APIVersion: "v1",}labelMap := make(map[string]string)labelMap["app"] = "this-is-my-crd"//可以配置pod的名字 Labels 等objectMeta := metav1.ObjectMeta{Name:   name,Labels: labelMap,}spec := crdv1.MyCrdGolangTestSpec{Mymsg:    "hello liuyijiang222",Mynumber: 123,Myarray:  []string{"AFFF", "BCCC", "CEEE"},}myCrdGolangTest := crdv1.MyCrdGolangTest{TypeMeta:   typeMeta,ObjectMeta: objectMeta,Spec:       spec,}crd, _ := clientset.LiuyjiangV1().MyCrdGolangTests(namespace).Create(context.TODO(), &myCrdGolangTest, metav1.CreateOptions{})fmt.Println(crd.GetName())
}/*
查询所有自定义资源  MyCrdGolangTest
*/
func listMyCrdGolangTest() string {clientset := getClientSet()namespace := "crd-golang-test"info := ""list, _ := clientset.LiuyjiangV1().MyCrdGolangTests(namespace).List(context.TODO(), metav1.ListOptions{})for _, cr := range list.Items {fmt.Println(cr.GetName())info = info + cr.GetName() + "\\n "}return info
}/*
查询单个自定义资源  MyCrdGolangTest
*/
func getMyCrdGolangTest(name string) *crdv1.MyCrdGolangTest {clientset := getClientSet()namespace := "crd-golang-test"cr, _ := clientset.LiuyjiangV1().MyCrdGolangTests(namespace).Get(context.TODO(), name, metav1.GetOptions{})//返回的cr 不会为空 只能根据名字是否有值去判断fmt.Println(cr.GetName())fmt.Println(cr.Labels)fmt.Println("=============Spec===============")fmt.Println("Mymsg: ", cr.Spec.Mymsg)fmt.Println("Mynumber: ", cr.Spec.Mynumber)fmt.Println("Myarray: ", cr.Spec.Myarray)fmt.Println("=============Status===============")fmt.Println("Mystatus: ", cr.Status.Mystatus)fmt.Println("Myip: ", cr.Status.Myip)return cr
}func getMyCrdGolangTestStr(name string) string {clientset := getClientSet()namespace := "crd-golang-test"cr, _ := clientset.LiuyjiangV1().MyCrdGolangTests(namespace).Get(context.TODO(), name, metav1.GetOptions{})info := "        " + cr.GetName() + " \\n "info = info + "=============Spec===============\\n Mymsg:" + cr.Spec.Mymsg + "\\n Mynumber: " + fmt.Sprintf("%d", cr.Spec.Mynumber)info = info + "\\n=============Status===============\\n Mystatus: " + cr.Status.Mystatus + " \\n"return info
}/*
更新自定义资源  MyCrdGolangTest
*/
func updateMyCrdGolangTest(name string) {fmt.Printf("=== before update === \\n\\n")cr := getMyCrdGolangTest(name)clientset := getClientSet()namespace := "default"cr.Spec.Mymsg = "ffffff"cr.Spec.Mynumber = 456cr.Spec.Myarray = []string{"TTTT", "GGGGG"}cr.Status.Myip = "127.0.0.1"cr.Status.Mystatus = "runing"clientset.LiuyjiangV1().MyCrdGolangTests(namespace).Update(context.TODO(), cr, metav1.UpdateOptions{})fmt.Printf("=== after update === \\n\\n")getMyCrdGolangTest(name)
}/*
监听资源
*/
func watchMyCrdGolangTest() {// 生成clientSetclientSet := getClientSet()/*使用Informer 实现watch操作*///使用NewSharedInformerFactory无法过滤 命名空间  无法根细粒度的选择哪个Crd//crdiformerFactory := crdexternalversions.NewSharedInformerFactory(clientSet, time.Second*30)/*NewFilteredSharedInformerFactory 可以过滤命名空间  和  Crd粒度*/namespace := "crd-golang-test"tweakListOptions := func(opt *metav1.ListOptions) {fmt.Println("=======tweakListOptions========")/这里还可以对pod进行筛选app=my-quarkus-demo*///opt.LabelSelector = "app=my-quarkus-demo"}crdiformerFactory := crdexternalversions.NewFilteredSharedInformerFactory(clientSet, time.Second*30, namespace, tweakListOptions)crdInformer := crdiformerFactory.Liuyjiang().V1().MyCrdGolangTests()crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{AddFunc: func(obj interface{}) {//fmt.Println(obj)crd := obj.(*crdv1.MyCrdGolangTest)fmt.Println("======AddFunc=====", crd.GetName())},UpdateFunc: func(oldObj, newObj interface{}) {//fmt.Println(oldObj)//fmt.Println(newObj)crd := newObj.(*crdv1.MyCrdGolangTest)fmt.Println("======UpdateFunc=====", crd.GetName())},DeleteFunc: func(obj interface{}) {crd := obj.(*crdv1.MyCrdGolangTest)fmt.Println("======DeleteFunc=====", crd.GetName())},})crdiformerFactory.Start(wait.NeverStop)crdiformerFactory.WaitForCacheSync(wait.NeverStop)//time.Sleep(time.Hour * 3)}

main.go

package mainimport ("context"crdweb "crdtest/pkg"crdv1 "crdtest/pkg/apis/liuyijiang.com/v1"crdclientset "crdtest/pkg/client/clientset/versioned"crdexternalversions "crdtest/pkg/client/informers/externalversions""fmt""time"metav1 "k8s.io/apimachinery/pkg/apis/meta/v1""k8s.io/apimachinery/pkg/util/wait""k8s.io/client-go/tools/cache""k8s.io/client-go/tools/clientcmd"
)//crdinformers "crdtest/pkg/client/informers/externalversions/liuyijiang.com/v1"
//crdexternalversions "crdtest/pkg/client/informers/externalversions"func main() {crdweb.StartWeb()
}

代码编写完成后使用交叉编译的方式,生成linux环境下可执行文件

GOOS=linux GOARCH=amd64 go build main.go

在这里插入图片描述

镜像创建

编写一个Dockerfile内容如下

FROM ubuntu
VOLUME ["/data/service/logs","/data/service/tmp"] 
WORKDIR "/data/service"
ENV LANG en_US.UTF-8  
ENV LANGUAGE en_US:en  
ENV LC_ALL en_US.UTF-8
COPY main main 
RUN chmod 711 main
ENTRYPOINT ["./main"]

使用 ubuntu作为基础镜像

然后创建镜像并推送到私库

docker build -t crd-go .
docker tag crd-go registry.cn-hangzhou.aliyuncs.com/jimliu/crd-go
docker push registry.cn-hangzhou.aliyuncs.com/jimliu/crd-go

在这里插入图片描述

此时得到可以使用的crd-go 版本镜像

权限配置与发布配置

权限和发布参考 《k8s java程序实现kubernetes Controller & Operator 使用CRD 学习总结》 文章中使用的CRD一致

直接贴出deploy.yml内容

# 创建命名空间
apiVersion: v1
kind: Namespace
metadata:name: crd-golang-testlabels:liuyijiang.com: crd-golang-test---# 创建阿里云私库秘钥
apiVersion: v1
kind: Secret
metadata:name: myaliyunsecret-crd-golang-testnamespace: crd-golang-testlabels:liuyijiang.com: crd-golang-test
data:.dockerconfigjson: eyJhdXRocyI6eyJyZWd-省略
type: kubernetes.io/dockerconfigjson---# 创建ServiceAccount 用于 程序中访问自定义资源
apiVersion: v1
kind: ServiceAccount
metadata:name: crd-golang-test-serviceaccountnamespace: crd-golang-testlabels:liuyijiang.com: crd-golang-test
imagePullSecrets:- name: myaliyunsecret-crd-golang-test---# 需要操作自定义的 CRD MyCrdGolangTest  需要配置对MyCrdGolangTest资源的操作权限
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:name: crd-golang-test-clusterrolelabels:liuyijiang.com: crd-golang-test
rules:- apiGroups:- "liuyjiang.com"  #apiGroups crd-liuyijiang.yaml中定义的 groupresources: - mycrdgolangtests  #MyCrdGolangTest  注意为crd-liuyijiang.yaml中配置的复数名称verbs:   #可以操作的类型- list- watch- get- create- delete - update - edit - exec---# 让ServiceAccount 与 ClusterRole绑定
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:name: crdtest-cluster-role-bindinglabels:liuyijiang.com: crd-golang-test
roleRef:apiGroup: rbac.authorization.k8s.iokind: ClusterRolename: crd-golang-test-clusterrole
subjects:- kind: ServiceAccountname: crd-golang-test-serviceaccountnamespace: crd-golang-test ---# 创建项目容器pod 
apiVersion: v1
kind: Pod
metadata: name: crd-gonamespace: crd-golang-test labels: app: crd-goliuyijiang.com: crd-golang-testspec: # 注意指定serviceAccountserviceAccountName: crd-golang-test-serviceaccountrestartPolicy: Alwayscontainers: - image:  registry.cn-hangzhou.aliyuncs.com/jimliu/crd-go:latestname: crd-go-runtime  ---# 创建service
apiVersion: v1
kind: Service
metadata:name: crd-go-servicenamespace: crd-golang-test 
spec:ports:- protocol: TCPport: 8000targetPort: 8000nodePort: 18000name: httpselector:app: crd-gotype: NodePort

部署与测试

kubectl apply -f deploy.yml 部署程序

在这里插入图片描述

调用创建接口 http://192.168.0.160:18000/create?name=jimtest

在这里插入图片描述

调用查询接口 http://192.168.0.160:18000/get?name=jimtest

在这里插入图片描述

手动修改一下cr

kubectl -n crd-golang-test edit mcgt jimtest

在这里插入图片描述

在这里插入图片描述

删除cr

在这里插入图片描述